From 67dcec10a1b422170c8c473484881174b9455729 Mon Sep 17 00:00:00 2001 From: AllyTally Date: Sun, 28 Jan 2024 21:29:22 -0400 Subject: [PATCH] Initial touch input support This commit adds virtual buttons on-screen to let you navigate through menus, and play the game. --- desktop_version/CMakeLists.txt | 1 + .../VVVVVV-android/app/build.gradle | 3 + desktop_version/src/FileSystemUtils.cpp | 2 + desktop_version/src/Game.cpp | 16 ++ desktop_version/src/Game.h | 1 + desktop_version/src/Graphics.cpp | 30 ++ desktop_version/src/Graphics.h | 3 + desktop_version/src/GraphicsResources.cpp | 8 + desktop_version/src/GraphicsResources.h | 5 + desktop_version/src/Input.cpp | 110 ++++++-- desktop_version/src/KeyPoll.cpp | 82 ++++++ desktop_version/src/KeyPoll.h | 2 + desktop_version/src/Render.cpp | 70 ++++- desktop_version/src/Script.cpp | 9 +- desktop_version/src/Touch.cpp | 262 ++++++++++++++++++ desktop_version/src/Touch.h | 63 +++++ desktop_version/src/main.cpp | 13 + desktop_version/src/preloader.cpp | 3 +- desktop_version/touch/buttons/button_left.png | Bin 0 -> 387 bytes desktop_version/touch/buttons/button_map.png | Bin 0 -> 275 bytes .../touch/buttons/button_right.png | Bin 0 -> 389 bytes desktop_version/touch/tutorial/arrowleft.png | Bin 0 -> 259 bytes desktop_version/touch/tutorial/arrowright.png | Bin 0 -> 267 bytes .../touch/tutorial/lefthand_far.png | Bin 0 -> 681 bytes .../touch/tutorial/lefthand_near.png | Bin 0 -> 720 bytes .../touch/tutorial/lefthand_off.png | Bin 0 -> 652 bytes .../touch/tutorial/righthand_far.png | Bin 0 -> 666 bytes .../touch/tutorial/righthand_near.png | Bin 0 -> 630 bytes .../touch/tutorial/righthand_off.png | Bin 0 -> 655 bytes .../touch/tutorial/touchscreen.png | Bin 0 -> 656 bytes 30 files changed, 638 insertions(+), 45 deletions(-) create mode 100644 desktop_version/src/Touch.cpp create mode 100644 desktop_version/src/Touch.h create mode 100644 desktop_version/touch/buttons/button_left.png create mode 100644 desktop_version/touch/buttons/button_map.png create mode 100644 desktop_version/touch/buttons/button_right.png create mode 100644 desktop_version/touch/tutorial/arrowleft.png create mode 100644 desktop_version/touch/tutorial/arrowright.png create mode 100644 desktop_version/touch/tutorial/lefthand_far.png create mode 100644 desktop_version/touch/tutorial/lefthand_near.png create mode 100644 desktop_version/touch/tutorial/lefthand_off.png create mode 100644 desktop_version/touch/tutorial/righthand_far.png create mode 100644 desktop_version/touch/tutorial/righthand_near.png create mode 100644 desktop_version/touch/tutorial/righthand_off.png create mode 100644 desktop_version/touch/tutorial/touchscreen.png diff --git a/desktop_version/CMakeLists.txt b/desktop_version/CMakeLists.txt index fbb33fef..dfce12a9 100644 --- a/desktop_version/CMakeLists.txt +++ b/desktop_version/CMakeLists.txt @@ -104,6 +104,7 @@ set(VVV_SRC src/TerminalScripts.cpp src/Textbox.cpp src/Tower.cpp + src/Touch.cpp src/UtilityClass.cpp src/WarpClass.cpp src/XMLUtils.cpp diff --git a/desktop_version/VVVVVV-android/app/build.gradle b/desktop_version/VVVVVV-android/app/build.gradle index 7eaecbdd..c40652e7 100644 --- a/desktop_version/VVVVVV-android/app/build.gradle +++ b/desktop_version/VVVVVV-android/app/build.gradle @@ -88,6 +88,9 @@ def zipRepoAssetsTask = tasks.register("zipRepoAssets", Zip) { from('../../lang') { spec -> spec.into('lang') } + from('../../touch') { spec -> + spec.into('graphics') + } archiveFileName.set('repo.zip') destinationDirectory.value(layout.buildDirectory.dir("generated/main/assets")) } diff --git a/desktop_version/src/FileSystemUtils.cpp b/desktop_version/src/FileSystemUtils.cpp index 5643182d..e617cd2c 100644 --- a/desktop_version/src/FileSystemUtils.cpp +++ b/desktop_version/src/FileSystemUtils.cpp @@ -314,6 +314,8 @@ int FILESYSTEM_init(char *argvZero, char* baseDir, char *assetsPath, char* langD doesFontsDirExist = mount_pre_datazip(NULL, "fonts", "graphics/", fontsDir); + mount_pre_datazip(NULL, "touch", "graphics/", NULL); + /* Mount the stock content last */ if (assetsPath) { diff --git a/desktop_version/src/Game.cpp b/desktop_version/src/Game.cpp index 1d7bad14..16d9d009 100644 --- a/desktop_version/src/Game.cpp +++ b/desktop_version/src/Game.cpp @@ -27,6 +27,7 @@ #include "RoomnameTranslator.h" #include "Screen.h" #include "Script.h" +#include "Touch.h" #include "Unused.h" #include "UTF8.h" #include "UtilityClass.h" @@ -4911,6 +4912,11 @@ void Game::deserializesettings(tinyxml2::XMLElement* dataNode, struct ScreenSett key.sensitivity = help.Int(pText); } + if (SDL_strcmp(pKey, "touchscale") == 0) + { + touch::scale = help.Int(pText); + } + if (SDL_strcmp(pKey, "lang") == 0) { loc::lang = std::string(pText); @@ -5189,6 +5195,8 @@ void Game::serializesettings(tinyxml2::XMLElement* dataNode, const struct Screen xml::update_tag(dataNode, "controllerSensitivity", key.sensitivity); + xml::update_tag(dataNode, "touchscale", touch::scale); + xml::update_tag(dataNode, "lang", loc::lang.c_str()); xml::update_tag(dataNode, "lang_set", (int) loc::lang_set); xml::update_tag(dataNode, "english_sprites", (int) loc::english_sprites); @@ -6819,6 +6827,7 @@ void Game::createmenu( enum Menu::MenuName t, bool samemenu/*= false*/ ) option(loc::gettext("graphics")); option(loc::gettext("audio")); option(loc::gettext("game pad")); + option(loc::gettext("touch input")); option(loc::gettext("accessibility")); option(loc::gettext("language"), !translator_cutscene_test); option(loc::gettext("return")); @@ -6891,6 +6900,13 @@ void Game::createmenu( enum Menu::MenuName t, bool samemenu/*= false*/ ) menuyoff = 0; maxspacing = 10; break; + case Menu::touch_input: + option(loc::gettext("control style"), false); + option(loc::gettext("ui scale")); + option(loc::gettext("return")); + menuyoff = 0; + maxspacing = 15; + break; case Menu::language: if (loc::languagelist.empty()) { diff --git a/desktop_version/src/Game.h b/desktop_version/src/Game.h index 362a956c..9838f85b 100644 --- a/desktop_version/src/Game.h +++ b/desktop_version/src/Game.h @@ -64,6 +64,7 @@ namespace Menu audiooptions, accessibility, controller, + touch_input, language, translator_main, translator_options, diff --git a/desktop_version/src/Graphics.cpp b/desktop_version/src/Graphics.cpp index 7d7489e5..b91167e5 100644 --- a/desktop_version/src/Graphics.cpp +++ b/desktop_version/src/Graphics.cpp @@ -19,6 +19,7 @@ #include "RoomnameTranslator.h" #include "Screen.h" #include "Script.h" +#include "Touch.h" #include "UtilityClass.h" #include "VFormat.h" #include "Vlogging.h" @@ -1174,6 +1175,31 @@ void Graphics::draw_texture(SDL_Texture* image, const int x, const int y) copy_texture(image, NULL, &dstrect); } +void Graphics::draw_texture(SDL_Texture* image, const int x, const int y, const int scalex, const int scaley) +{ + int w, h; + + if (query_texture(image, NULL, NULL, &w, &h) != 0) + { + return; + } + + int flip = SDL_FLIP_NONE; + + if (scalex < 0) + { + flip |= SDL_FLIP_HORIZONTAL; + } + if (scaley < 0) + { + flip |= SDL_FLIP_VERTICAL; + } + + const SDL_Rect dstrect = { x, y, w * SDL_abs(scalex), h * SDL_abs(scaley) }; + + copy_texture(image, NULL, &dstrect, 0, NULL, (SDL_RendererFlip)flip); +} + void Graphics::draw_texture_part(SDL_Texture* image, const int x, const int y, const int x2, const int y2, const int w, const int h, const int scalex, const int scaley) { const SDL_Rect srcrect = {x2, y2, w, h}; @@ -3464,6 +3490,8 @@ void Graphics::screenshake(void) get_stretch_info(&rect); copy_texture(tempShakeTexture, NULL, &rect, 0, NULL, flipmode ? SDL_FLIP_VERTICAL : SDL_FLIP_NONE); + + touch::render(); } void Graphics::updatescreenshake(void) @@ -3545,6 +3573,8 @@ void Graphics::render(void) get_stretch_info(&rect); copy_texture(gameTexture, NULL, &rect, 0, NULL, flipmode ? SDL_FLIP_VERTICAL : SDL_FLIP_NONE); + + touch::render(); } void Graphics::renderwithscreeneffects(void) diff --git a/desktop_version/src/Graphics.h b/desktop_version/src/Graphics.h index c37622a2..fe1274c4 100644 --- a/desktop_version/src/Graphics.h +++ b/desktop_version/src/Graphics.h @@ -161,6 +161,8 @@ public: void draw_texture(SDL_Texture* image, int x, int y); + void draw_texture(SDL_Texture* image, int x, int y, int scalex, int scaley); + void draw_texture_part(SDL_Texture* image, int x, int y, int x2, int y2, int w, int h, int scalex, int scaley); void draw_grid_tile(SDL_Texture* texture, int t, int x, int y, int width, int height, int scalex, int scaley); @@ -259,6 +261,7 @@ public: int screenshake_y; void draw_window_background(void); + void draw_touch(void); void get_stretch_info(SDL_Rect* rect); diff --git a/desktop_version/src/GraphicsResources.cpp b/desktop_version/src/GraphicsResources.cpp index 571a1f00..49364cdc 100644 --- a/desktop_version/src/GraphicsResources.cpp +++ b/desktop_version/src/GraphicsResources.cpp @@ -429,6 +429,10 @@ void GraphicsResources::init(void) im_image10 = LoadImage("graphics/ending.png"); im_image11 = LoadImage("graphics/site4.png", TEX_WHITE); + im_button_left = LoadImage("graphics/buttons/button_left.png"); + im_button_right = LoadImage("graphics/buttons/button_right.png"); + im_button_map = LoadImage("graphics/buttons/button_map.png"); + im_sprites_translated = NULL; im_flipsprites_translated = NULL; @@ -476,6 +480,10 @@ void GraphicsResources::destroy(void) CLEAR(im_sprites_translated); CLEAR(im_flipsprites_translated); + + CLEAR(im_button_left); + CLEAR(im_button_right); + CLEAR(im_button_map); #undef CLEAR VVV_freefunc(SDL_FreeSurface, im_sprites_surf); diff --git a/desktop_version/src/GraphicsResources.h b/desktop_version/src/GraphicsResources.h index f7973c68..a2ed89ab 100644 --- a/desktop_version/src/GraphicsResources.h +++ b/desktop_version/src/GraphicsResources.h @@ -48,6 +48,11 @@ public: SDL_Texture* im_sprites_translated; SDL_Texture* im_flipsprites_translated; + + /* Touch */ + SDL_Texture* im_button_left; + SDL_Texture* im_button_right; + SDL_Texture* im_button_map; }; SDL_Surface* LoadImageSurface(const char* filename); diff --git a/desktop_version/src/Input.cpp b/desktop_version/src/Input.cpp index aab9a9a6..476eba11 100644 --- a/desktop_version/src/Input.cpp +++ b/desktop_version/src/Input.cpp @@ -23,6 +23,7 @@ #include "RoomnameTranslator.h" #include "Screen.h" #include "Script.h" +#include "Touch.h" #include "UtilityClass.h" #include "Vlogging.h" @@ -1055,12 +1056,18 @@ static void menuactionpress(void) map.nexttowercolour(); break; case 4: + // touch input options + music.playef(Sound_VIRIDIAN); + game.createmenu(Menu::touch_input); + map.nexttowercolour(); + break; + case 5: //accessibility options music.playef(Sound_VIRIDIAN); game.createmenu(Menu::accessibility); map.nexttowercolour(); break; - case 5: + case 6: //language options if (game.translator_cutscene_test) { @@ -1975,6 +1982,28 @@ static void menuactionpress(void) break; } break; + case Menu::touch_input: + switch (game.currentmenuoption) + { + case 0: + music.playef(Sound_CRY); + break; + case 1: + touch::scale += 0.5; + music.playef(Sound_VIRIDIAN); + if (touch::scale > 2) + { + touch::scale = 0.5; + } + game.savestatsandsettings_menu(); + break; + case 2: + music.playef(Sound_VIRIDIAN); + game.returnmenu(); + map.nexttowercolour(); + break; + } + break; case Menu::cleardatamenu: switch (game.currentmenuoption) { @@ -2347,26 +2376,37 @@ void titleinput(void) controller_down |= key.controllerWantsLeft(false); } - if (key.isDown(left) || key.isDown(KEYBOARD_UP) || key.isDown(a) || key.isDown(KEYBOARD_w) || controller_up) + if (key.isDown(left) || key.isDown(KEYBOARD_UP) || key.isDown(a) || key.isDown(KEYBOARD_w) || controller_up || touch::button_tapped(TOUCH_BUTTON_LEFT)) { game.press_left = true; } - if (key.isDown(right) || key.isDown(KEYBOARD_DOWN) || key.isDown(d) || key.isDown(KEYBOARD_s) || controller_down) + if (key.isDown(right) || key.isDown(KEYBOARD_DOWN) || key.isDown(d) || key.isDown(KEYBOARD_s) || controller_down || touch::button_tapped(TOUCH_BUTTON_RIGHT)) { game.press_right = true; } } - if (key.isDown(KEYBOARD_z) || key.isDown(KEYBOARD_SPACE) || key.isDown(KEYBOARD_v) || key.isDown(game.controllerButton_flip)) game.press_action = true; + if (key.isDown(KEYBOARD_z) || key.isDown(KEYBOARD_SPACE) || key.isDown(KEYBOARD_v) || key.isDown(game.controllerButton_flip) + || (!game.menustart ? touch::screen_tapped() : touch::button_tapped(TOUCH_BUTTON_CONFIRM))) + { + game.press_action = true; + } + //|| key.isDown(KEYBOARD_UP) || key.isDown(KEYBOARD_DOWN)) game.press_action = true; //on menus, up and down don't work as action if (key.isDown(KEYBOARD_ENTER)) game.press_map = true; //In the menu system, all keypresses are single taps rather than holds. Therefore this test has to be done for all presses - if (!game.press_action && !game.press_left && !game.press_right && !key.isDown(27) && !key.isDown(game.controllerButton_esc)) game.jumpheld = false; + if (!game.press_action && !game.press_left && !game.press_right && !key.isDown(27) && !key.isDown(game.controllerButton_esc) + && !touch::button_tapped(TOUCH_BUTTON_CANCEL)) + { + game.jumpheld = false; + } + if (!game.press_map) game.mapheld = false; if (!game.jumpheld && graphics.fademode == FADE_NONE) { - if (game.press_action || game.press_left || game.press_right || game.press_map || key.isDown(27) || key.isDown(game.controllerButton_esc)) + if (game.press_action || game.press_left || game.press_right || game.press_map || key.isDown(27) || key.isDown(game.controllerButton_esc) + || touch::button_tapped(TOUCH_BUTTON_CANCEL)) { game.jumpheld = true; } @@ -2385,7 +2425,7 @@ void titleinput(void) if (game.menustart && game.menucountdown <= 0 - && (key.isDown(27) || key.isDown(game.controllerButton_esc))) + && (key.isDown(27) || key.isDown(game.controllerButton_esc) || touch::button_tapped(TOUCH_BUTTON_CANCEL))) { if (game.currentmenuname == Menu::language && loc::pre_title_lang_menu) { @@ -2562,16 +2602,17 @@ void gameinput(void) game.press_action = false; game.press_interact = false; - if (key.isDown(KEYBOARD_LEFT) || key.isDown(KEYBOARD_a) || key.controllerWantsLeft(false)) + if (key.isDown(KEYBOARD_LEFT) || key.isDown(KEYBOARD_a) || key.controllerWantsLeft(false) || touch::buttons[TOUCH_BUTTON_LEFT].down) { game.press_left = true; } - if (key.isDown(KEYBOARD_RIGHT) || key.isDown(KEYBOARD_d) || key.controllerWantsRight(false)) + if (key.isDown(KEYBOARD_RIGHT) || key.isDown(KEYBOARD_d) || key.controllerWantsRight(false) || touch::buttons[TOUCH_BUTTON_RIGHT].down) { game.press_right = true; } if (key.isDown(KEYBOARD_z) || key.isDown(KEYBOARD_SPACE) || key.isDown(KEYBOARD_v) - || key.isDown(KEYBOARD_UP) || key.isDown(KEYBOARD_DOWN) || key.isDown(KEYBOARD_w) || key.isDown(KEYBOARD_s)|| key.isDown(game.controllerButton_flip)) + || key.isDown(KEYBOARD_UP) || key.isDown(KEYBOARD_DOWN) || key.isDown(KEYBOARD_w) + || key.isDown(KEYBOARD_s) || key.isDown(game.controllerButton_flip) || touch::touching_right()) { game.press_action = true; } @@ -2583,7 +2624,7 @@ void gameinput(void) } game.press_map = false; - if (key.isDown(KEYBOARD_ENTER) || key.isDown(SDLK_KP_ENTER) || key.isDown(game.controllerButton_map) ) + if (key.isDown(KEYBOARD_ENTER) || key.isDown(SDLK_KP_ENTER) || key.isDown(game.controllerButton_map) || touch::button_tapped(TOUCH_BUTTON_MAP)) { game.press_map = true; } @@ -2600,7 +2641,12 @@ void gameinput(void) { game.press_action = false; if (key.isDown(KEYBOARD_z) || key.isDown(KEYBOARD_SPACE) || key.isDown(KEYBOARD_v) - || key.isDown(KEYBOARD_UP) || key.isDown(KEYBOARD_DOWN) || key.isDown(KEYBOARD_w) || key.isDown(KEYBOARD_s) || key.isDown(game.controllerButton_flip)) game.press_action = true; + || key.isDown(KEYBOARD_UP) || key.isDown(KEYBOARD_DOWN) || key.isDown(KEYBOARD_w) + || key.isDown(KEYBOARD_s) || key.isDown(game.controllerButton_flip) || touch::screen_tapped() + ) + { + game.press_action = true; + } } if (game.press_action && !game.jumpheld) @@ -2632,7 +2678,8 @@ void gameinput(void) //immediately open again //We really need a better input system soon... && !key.isDown(27) - && !key.isDown(game.controllerButton_esc)) + && !key.isDown(game.controllerButton_esc) + && !touch::button_tapped(TOUCH_BUTTON_CANCEL)) { game.mapheld = false; } @@ -2977,7 +3024,7 @@ void gameinput(void) } if (!game.mapheld - && (key.isDown(27) || key.isDown(game.controllerButton_esc)) + && (key.isDown(27) || key.isDown(game.controllerButton_esc) || touch::button_tapped(TOUCH_BUTTON_CANCEL)) && (!map.custommode || map.custommodeforreal)) { game.mapheld = true; @@ -3099,15 +3146,15 @@ void mapinput(void) controller_down |= key.controllerWantsLeft(false); } - if (key.isDown(left) || key.isDown(KEYBOARD_UP) || key.isDown(a) || key.isDown(KEYBOARD_w)|| controller_up) + if (key.isDown(left) || key.isDown(KEYBOARD_UP) || key.isDown(a) || key.isDown(KEYBOARD_w)|| controller_up || touch::button_tapped(TOUCH_BUTTON_LEFT)) { game.press_left = true; } - if (key.isDown(right) || key.isDown(KEYBOARD_DOWN) || key.isDown(d) || key.isDown(KEYBOARD_s)|| controller_down) + if (key.isDown(right) || key.isDown(KEYBOARD_DOWN) || key.isDown(d) || key.isDown(KEYBOARD_s)|| controller_down || touch::button_tapped(TOUCH_BUTTON_RIGHT)) { game.press_right = true; } - if (key.isDown(KEYBOARD_z) || key.isDown(KEYBOARD_SPACE) || key.isDown(KEYBOARD_v) || key.isDown(game.controllerButton_flip)) + if (key.isDown(KEYBOARD_z) || key.isDown(KEYBOARD_SPACE) || key.isDown(KEYBOARD_v) || key.isDown(game.controllerButton_flip) || touch::button_tapped(TOUCH_BUTTON_CONFIRM)) { game.press_action = true; } @@ -3115,8 +3162,8 @@ void mapinput(void) || (game.menupage >= 20 && game.menupage <= 21) || (game.menupage >= 30 && game.menupage <= 32)) { - if (key.isDown(KEYBOARD_ENTER) || key.isDown(game.controllerButton_map) ) game.press_map = true; - if (key.isDown(27) && !game.mapheld) + if (key.isDown(KEYBOARD_ENTER) || key.isDown(game.controllerButton_map) || touch::button_tapped(TOUCH_BUTTON_CONFIRM)) game.press_map = true; + if ((key.isDown(27) || touch::button_tapped(TOUCH_BUTTON_CANCEL)) && !game.mapheld) { game.mapheld = true; if (game.menupage < 9 @@ -3137,7 +3184,11 @@ void mapinput(void) } else { - if (key.isDown(KEYBOARD_ENTER) || key.isDown(27)|| key.isDown(game.controllerButton_map) ) game.press_map = true; + if (key.isDown(KEYBOARD_ENTER) || key.isDown(27) || key.isDown(game.controllerButton_map) + || touch::button_tapped(TOUCH_BUTTON_CANCEL) || touch::button_tapped(TOUCH_BUTTON_CONFIRM)) + { + game.press_map = true; + } } //In the menu system, all keypresses are single taps rather than holds. Therefore this test has to be done for all presses @@ -3338,11 +3389,16 @@ void teleporterinput(void) if(graphics.menuoffset==0) { - if (key.isDown(KEYBOARD_LEFT)|| key.isDown(KEYBOARD_a) || key.controllerWantsLeft(false) ) game.press_left = true; - if (key.isDown(KEYBOARD_RIGHT) || key.isDown(KEYBOARD_d)|| key.controllerWantsRight(false) ) game.press_right = true; + if (key.isDown(KEYBOARD_LEFT)|| key.isDown(KEYBOARD_a) || key.controllerWantsLeft(false) || touch::button_tapped(TOUCH_BUTTON_LEFT)) game.press_left = true; + if (key.isDown(KEYBOARD_RIGHT) || key.isDown(KEYBOARD_d)|| key.controllerWantsRight(false) || touch::button_tapped(TOUCH_BUTTON_RIGHT)) game.press_right = true; if (key.isDown(KEYBOARD_z) || key.isDown(KEYBOARD_SPACE) || key.isDown(KEYBOARD_v) - || key.isDown(KEYBOARD_UP) || key.isDown(KEYBOARD_DOWN)|| key.isDown(KEYBOARD_w)|| key.isDown(KEYBOARD_s) || key.isDown(game.controllerButton_flip)) game.press_action = true; - if (!game.separate_interact && (key.isDown(KEYBOARD_ENTER) || key.isDown(game.controllerButton_map))) + || key.isDown(KEYBOARD_UP) || key.isDown(KEYBOARD_DOWN) || key.isDown(KEYBOARD_w) + || key.isDown(KEYBOARD_s) || key.isDown(game.controllerButton_flip) + || touch::button_tapped(TOUCH_BUTTON_CONFIRM)) + { + game.press_action = true; + } + if (!game.separate_interact && (key.isDown(KEYBOARD_ENTER) || key.isDown(game.controllerButton_map) || touch::button_tapped(TOUCH_BUTTON_CONFIRM))) { game.press_map = true; } @@ -3355,7 +3411,7 @@ void teleporterinput(void) if (!game.press_action && !game.press_left && !game.press_right && !game.press_interact) game.jumpheld = false; if (!game.press_map) game.mapheld = false; - if (key.isDown(27)) + if (key.isDown(27) || touch::button_tapped(TOUCH_BUTTON_CANCEL)) { if (!map.custommode || map.custommodeforreal) { @@ -3482,7 +3538,7 @@ void gamecompleteinput(void) graphics.titlebg.bypos += graphics.titlebg.bscroll; game.oldcreditposition = game.creditposition; - if (key.isDown(KEYBOARD_z) || key.isDown(KEYBOARD_SPACE) || key.isDown(KEYBOARD_v) || key.isDown(game.controllerButton_flip)) + if (key.isDown(KEYBOARD_z) || key.isDown(KEYBOARD_SPACE) || key.isDown(KEYBOARD_v) || key.isDown(game.controllerButton_flip) || touch::screen_tapped()) { game.creditposition -= 6; if (game.creditposition <= -Credits::creditmaxposition) @@ -3530,7 +3586,7 @@ void gamecompleteinput2(void) //Do this here because input comes first game.oldcreditposx = game.creditposx; - if (key.isDown(KEYBOARD_z) || key.isDown(KEYBOARD_SPACE) || key.isDown(KEYBOARD_v) || key.isDown(game.controllerButton_flip)) + if (key.isDown(KEYBOARD_z) || key.isDown(KEYBOARD_SPACE) || key.isDown(KEYBOARD_v) || key.isDown(game.controllerButton_flip) || touch::screen_tapped()) { game.creditposx++; game.oldcreditposx++; diff --git a/desktop_version/src/KeyPoll.cpp b/desktop_version/src/KeyPoll.cpp index b56b9d47..ab3f93b2 100644 --- a/desktop_version/src/KeyPoll.cpp +++ b/desktop_version/src/KeyPoll.cpp @@ -17,6 +17,7 @@ #include "LocalizationStorage.h" #include "Music.h" #include "Screen.h" +#include "Touch.h" #include "UTF8.h" #include "UtilityClass.h" #include "Vlogging.h" @@ -60,6 +61,8 @@ KeyPoll::KeyPoll(void) linealreadyemptykludge = false; isActive = true; + + using_touch = false; } void KeyPoll::enabletextentry(void) @@ -210,6 +213,20 @@ bool cycle_language(bool should_recompute_textboxes) return should_recompute_textboxes; } +static void remove_finger(int i) +{ + for (int j = 0; j < NUM_TOUCH_BUTTONS; j++) + { + if (touch::buttons[j].fingerId == touch::fingers[i].id) + { + touch::buttons[j].down = false; + touch::buttons[j].fingerId = -1; + } + } + + touch::fingers.erase(touch::fingers.begin() + i); +} + void KeyPoll::Poll(void) { static int raw_mousex = 0; @@ -224,6 +241,12 @@ void KeyPoll::Poll(void) bool should_recompute_textboxes = false; bool active_input_device_changed = false; bool keyboard_was_active = BUTTONGLYPHS_keyboard_is_active(); + int screen_width; + int screen_height; + gameScreen.GetScreenSize(&screen_width, &screen_height); + + touch::reset(); + while (SDL_PollEvent(&evt)) { switch (evt.type) @@ -439,6 +462,61 @@ void KeyPoll::Poll(void) break; } + /* Touch Events */ + case SDL_FINGERDOWN: + { + using_touch = true; + + VVV_Finger finger; + finger.pressed = true; + finger.x = evt.tfinger.x * screen_width; + finger.y = evt.tfinger.y * screen_height; + finger.id = evt.tfinger.fingerId; + finger.on_button = false; + touch::fingers.push_back(finger); + + raw_mousex = evt.tfinger.x * screen_width; + raw_mousey = evt.tfinger.y * screen_height; + leftbutton = 1; + break; + } + case SDL_FINGERMOTION: + { + using_touch = true; + + for (int i = 0; i < (int) touch::fingers.size(); i++) + { + if (touch::fingers[i].id == evt.tfinger.fingerId) + { + touch::fingers[i].x = evt.tfinger.x * screen_width; + touch::fingers[i].y = evt.tfinger.y * screen_height; + break; + } + } + + raw_mousex = evt.tfinger.x * screen_width; + raw_mousey = evt.tfinger.y * screen_height; + break; + } + case SDL_FINGERUP: + { + using_touch = true; + + for (int i = (int) touch::fingers.size() - 1; i >= 0; i--) + { + if (touch::fingers[i].id == evt.tfinger.fingerId) + { + // Unpress any buttons that this finger may belong to + remove_finger(i); + } + } + + raw_mousex = evt.tfinger.x * screen_width; + raw_mousey = evt.tfinger.y * screen_height; + leftbutton = 0; + break; + } + /* Window Events */ case SDL_WINDOWEVENT: switch (evt.window.event) @@ -520,6 +598,7 @@ void KeyPoll::Poll(void) switch (evt.type) { case SDL_KEYDOWN: + using_touch = false; if (evt.key.repeat == 0) { hidemouse = true; @@ -528,6 +607,7 @@ void KeyPoll::Poll(void) case SDL_TEXTINPUT: case SDL_CONTROLLERBUTTONDOWN: case SDL_CONTROLLERAXISMOTION: + using_touch = false; hidemouse = true; break; case SDL_MOUSEMOTION: @@ -560,6 +640,8 @@ void KeyPoll::Poll(void) { recomputetextboxes(); } + + touch::update_buttons(); } bool KeyPoll::isDown(SDL_Keycode key) diff --git a/desktop_version/src/KeyPoll.h b/desktop_version/src/KeyPoll.h index 5718ebf0..aaa939f4 100644 --- a/desktop_version/src/KeyPoll.h +++ b/desktop_version/src/KeyPoll.h @@ -72,6 +72,8 @@ public: bool linealreadyemptykludge; + bool using_touch; + private: std::map controllers; std::map buttonmap; diff --git a/desktop_version/src/Render.cpp b/desktop_version/src/Render.cpp index 82132350..f43dc26d 100644 --- a/desktop_version/src/Render.cpp +++ b/desktop_version/src/Render.cpp @@ -25,6 +25,7 @@ #include "RoomnameTranslator.h" #include "Screen.h" #include "Script.h" +#include "Touch.h" #include "UtilityClass.h" #include "VFormat.h" @@ -391,10 +392,14 @@ static void menurender(void) font::print_wrap(PR_CEN, -1, 65, loc::gettext("Rebind your controller's buttons and adjust sensitivity."), tr, tg, tb); break; case 4: + font::print(PR_2X | PR_CEN, -1, 30, loc::gettext("Touch Input"), tr, tg, tb); + font::print_wrap(PR_CEN, -1, 65, loc::gettext("Change touch input options."), tr, tg, tb); + break; + case 5: font::print(PR_2X | PR_CEN, -1, 30, loc::gettext("Accessibility"), tr, tg, tb); font::print_wrap(PR_CEN, -1, 65, loc::gettext("Disable screen effects, enable slowdown modes or invincibility."), tr, tg, tb); break; - case 5: + case 6: font::print(PR_2X | PR_CEN, -1, 30, loc::gettext("Language"), tr, tg, tb); font::print_wrap(PR_CEN, -1, 65, loc::gettext("Change the language."), tr, tg, tb); } @@ -763,6 +768,31 @@ static void menurender(void) break; } + case Menu::touch_input: + { + switch (game.currentmenuoption) + { + case 0: // Control style + font::print(PR_2X | PR_CEN, -1, 30, loc::gettext("Control Style"), tr, tg, tb); + font::print_wrap(PR_CEN, -1, 65, loc::gettext("Change the control style for touch input."), tr, tg, tb); + break; + case 1: + // Display touch buttons! + key.using_touch = true; + + font::print(PR_2X | PR_CEN, -1, 30, loc::gettext("UI Scale"), tr, tg, tb); + font::print_wrap(PR_CEN, -1, 65, loc::gettext("Change the scale of the UI buttons."), tr, tg, tb); + + char buffer[SCREEN_WIDTH_CHARS + 1]; + vformat_buf(buffer, sizeof(buffer), loc::gettext("Current scale: {scale}.{extra}x"), "scale:int, extra:int", + (int)touch::scale, + (int)((float)((float)touch::scale - (int)touch::scale) * 10) + ); + font::print(PR_CEN, -1, 75, buffer, tr, tg, tb); + break; + } + } + break; case Menu::language: if (loc::languagelist.empty()) { @@ -2432,12 +2462,19 @@ void gamerender(void) if (game.advancetext) { char buffer_adv[SCREEN_WIDTH_CHARS + 1]; - vformat_buf( - buffer_adv, sizeof(buffer_adv), - loc::gettext("- Press {button} to advance text -"), - "button:but", - vformat_button(ActionSet_InGame, Action_InGame_ACTION) - ); + if (key.using_touch) + { + SDL_strlcpy(buffer_adv, loc::gettext("- Tap screen to advance text -"), sizeof(buffer_adv)); + } + else + { + vformat_buf( + buffer_adv, sizeof(buffer_adv), + loc::gettext("- Press {button} to advance text -"), + "button:but", + vformat_button(ActionSet_InGame, Action_InGame_ACTION) + ); + } font::print(PR_CEN | PR_BOR, -1, graphics.flipmode ? 228 : 5, buffer_adv, 220 - (help.glow), 220 - (help.glow), 255 - (help.glow / 2)); } @@ -3507,12 +3544,19 @@ void teleporterrender(void) if (game.advancetext) { char buffer_adv[SCREEN_WIDTH_CHARS + 1]; - vformat_buf( - buffer_adv, sizeof(buffer_adv), - loc::gettext("- Press {button} to advance text -"), - "button:but", - vformat_button(ActionSet_InGame, Action_InGame_ACTION) - ); + if (key.using_touch) + { + SDL_strlcpy(buffer_adv, loc::gettext("- Tap screen to advance text -"), sizeof(buffer_adv)); + } + else + { + vformat_buf( + buffer_adv, sizeof(buffer_adv), + loc::gettext("- Press {button} to advance text -"), + "button:but", + vformat_button(ActionSet_InGame, Action_InGame_ACTION) + ); + } font::print(PR_CEN | PR_BOR, -1, graphics.flipmode ? 228 : 5, buffer_adv, 220 - (help.glow), 220 - (help.glow), 255 - (help.glow / 2)); } diff --git a/desktop_version/src/Script.cpp b/desktop_version/src/Script.cpp index a58b1735..d013a55f 100644 --- a/desktop_version/src/Script.cpp +++ b/desktop_version/src/Script.cpp @@ -20,6 +20,7 @@ #include "LocalizationStorage.h" #include "Map.h" #include "Music.h" +#include "Touch.h" #include "Unreachable.h" #include "UtilityClass.h" #include "VFormat.h" @@ -802,7 +803,7 @@ void scriptclass::run(void) game.hascontrol = false; game.pausescript = true; if (key.isDown(90) || key.isDown(32) || key.isDown(86) - || key.isDown(KEYBOARD_UP) || key.isDown(KEYBOARD_DOWN)) game.jumpheld = true; + || key.isDown(KEYBOARD_UP) || key.isDown(KEYBOARD_DOWN) || touch::screen_tapped()) game.jumpheld = true; } game.backgroundtext = false; @@ -1828,7 +1829,7 @@ void scriptclass::run(void) game.hascontrol = false; game.pausescript = true; if (key.isDown(90) || key.isDown(32) || key.isDown(86) - || key.isDown(KEYBOARD_UP) || key.isDown(KEYBOARD_DOWN)) game.jumpheld = true; + || key.isDown(KEYBOARD_UP) || key.isDown(KEYBOARD_DOWN) || touch::screen_tapped()) game.jumpheld = true; } game.backgroundtext = false; } @@ -1851,7 +1852,7 @@ void scriptclass::run(void) game.hascontrol = false; game.pausescript = true; if (key.isDown(90) || key.isDown(32) || key.isDown(86) - || key.isDown(KEYBOARD_UP) || key.isDown(KEYBOARD_DOWN)) game.jumpheld = true; + || key.isDown(KEYBOARD_UP) || key.isDown(KEYBOARD_DOWN) || touch::screen_tapped()) game.jumpheld = true; } game.backgroundtext = false; } @@ -1872,7 +1873,7 @@ void scriptclass::run(void) game.hascontrol = false; game.pausescript = true; if (key.isDown(90) || key.isDown(32) || key.isDown(86) - || key.isDown(KEYBOARD_UP) || key.isDown(KEYBOARD_DOWN)) game.jumpheld = true; + || key.isDown(KEYBOARD_UP) || key.isDown(KEYBOARD_DOWN) || touch::screen_tapped()) game.jumpheld = true; } game.backgroundtext = false; } diff --git a/desktop_version/src/Touch.cpp b/desktop_version/src/Touch.cpp new file mode 100644 index 00000000..939be707 --- /dev/null +++ b/desktop_version/src/Touch.cpp @@ -0,0 +1,262 @@ +#include "Touch.h" + +#include +#include + +#include "Constants.h" +#include "Graphics.h" +#include "GraphicsResources.h" +#include "KeyPoll.h" +#include "Screen.h" +#include "Script.h" +#include "UtilityClass.h" + +namespace touch +{ + std::vector fingers; + TouchButton buttons[NUM_TOUCH_BUTTONS]; + float scale; + + int get_rect(TouchButton* button, SDL_Rect* rect) + { + rect->x = button->x; + rect->y = button->y; + rect->w = button->width; + rect->h = button->height; + + return 0; + } + + int get_scale(void) + { + SDL_Rect rect; + graphics.get_stretch_info(&rect); + + int scale_x = rect.w / SCREEN_WIDTH_PIXELS; + int scale_y = rect.h / SCREEN_HEIGHT_PIXELS; + + return SDL_ceil(SDL_min(scale_x, scale_y) * scale); + } + + void init(void) + { + scale = 1; + + for (int i = 0; i < NUM_TOUCH_BUTTONS; i++) + { + buttons[i].image = NULL; + buttons[i].active = false; + buttons[i].down = false; + buttons[i].fingerId = -1; + } + } + + void refresh_buttons(void) + { + int width; + int height; + int scale = get_scale(); + + gameScreen.GetScreenSize(&width, &height); + + buttons[TOUCH_BUTTON_LEFT].x = 0; + buttons[TOUCH_BUTTON_LEFT].y = height - (40 * scale) - 8; + buttons[TOUCH_BUTTON_LEFT].width = 40 * scale; + buttons[TOUCH_BUTTON_LEFT].height = 40 * scale; + buttons[TOUCH_BUTTON_LEFT].image = graphics.grphx.im_button_left; + + buttons[TOUCH_BUTTON_RIGHT].x = (40 * scale) + 8; + buttons[TOUCH_BUTTON_RIGHT].y = height - (40 * scale) - 8; + buttons[TOUCH_BUTTON_RIGHT].width = 40 * scale; + buttons[TOUCH_BUTTON_RIGHT].height = 40 * scale; + buttons[TOUCH_BUTTON_RIGHT].image = graphics.grphx.im_button_right; + + buttons[TOUCH_BUTTON_MAP].x = width - (35 * scale); + buttons[TOUCH_BUTTON_MAP].y = 0; + buttons[TOUCH_BUTTON_MAP].width = 35 * scale; + buttons[TOUCH_BUTTON_MAP].height = 30 * scale; + buttons[TOUCH_BUTTON_MAP].image = graphics.grphx.im_button_map; + + buttons[TOUCH_BUTTON_CANCEL].x = width - (40 * scale); + buttons[TOUCH_BUTTON_CANCEL].y = height - (40 * scale * 2) - 16; + buttons[TOUCH_BUTTON_CANCEL].width = 40 * scale; + buttons[TOUCH_BUTTON_CANCEL].height = 40 * scale; + buttons[TOUCH_BUTTON_CANCEL].image = graphics.grphx.im_button_left; + + buttons[TOUCH_BUTTON_CONFIRM].x = width - (40 * scale); + buttons[TOUCH_BUTTON_CONFIRM].y = height - (40 * scale) - 8; + buttons[TOUCH_BUTTON_CONFIRM].width = 40 * scale; + buttons[TOUCH_BUTTON_CONFIRM].height = 40 * scale; + buttons[TOUCH_BUTTON_CONFIRM].image = graphics.grphx.im_button_right; + + // First, reset all buttons + for (int i = 0; i < NUM_TOUCH_BUTTONS; i++) + { + buttons[i].active = false; + } + + // Now, set the buttons that are active + + switch (game.gamestate) + { + case GAMEMODE: + if (!script.running && game.hascontrol) + { + buttons[TOUCH_BUTTON_LEFT].active = true; + buttons[TOUCH_BUTTON_RIGHT].active = true; + buttons[TOUCH_BUTTON_MAP].active = true; + } + break; + + case TITLEMODE: + if (game.menustart) + { + buttons[TOUCH_BUTTON_LEFT].active = true; + buttons[TOUCH_BUTTON_RIGHT].active = true; + buttons[TOUCH_BUTTON_CANCEL].active = true; + buttons[TOUCH_BUTTON_CONFIRM].active = true; + } + break; + case TELEPORTERMODE: + if (game.useteleporter) + { + buttons[TOUCH_BUTTON_LEFT].active = true; + buttons[TOUCH_BUTTON_RIGHT].active = true; + buttons[TOUCH_BUTTON_CANCEL].active = true; + buttons[TOUCH_BUTTON_CONFIRM].active = true; + } + break; + case MAPMODE: + buttons[TOUCH_BUTTON_LEFT].active = true; + buttons[TOUCH_BUTTON_RIGHT].active = true; + buttons[TOUCH_BUTTON_CANCEL].active = true; + buttons[TOUCH_BUTTON_CONFIRM].active = true; + break; + case GAMECOMPLETE: + case GAMECOMPLETE2: + case EDITORMODE: + case PRELOADER: + default: + break; + } + } + + void render(void) + { + if (!key.using_touch) + { + return; + } + + int scale = get_scale(); + refresh_buttons(); + + for (int i = 0; i < NUM_TOUCH_BUTTONS; i++) + { + SDL_Rect rect; + get_rect(&buttons[i], &rect); + + if (buttons[i].image != NULL && buttons[i].active) + { + graphics.draw_texture(buttons[i].image, rect.x, rect.y + (buttons[i].down ? 2 * scale : 0), scale, scale); + } + } + } + + void reset(void) + { + for (int i = 0; i < NUM_TOUCH_BUTTONS; i++) + { + buttons[i].down = false; + buttons[i].fingerId = -1; + } + + for (int i = 0; i < fingers.size(); i++) + { + fingers[i].pressed = false; + fingers[i].on_button = false; + } + } + + void update_buttons(void) + { + if (graphics.fademode != FADE_NONE) + { + return; + } + + SDL_Point point; + SDL_Rect rect; + + for (int buttonId = 0; buttonId < NUM_TOUCH_BUTTONS; buttonId++) + { + TouchButton* button = &buttons[buttonId]; + button->down = false; + + for (int fingerId = 0; fingerId < fingers.size(); fingerId++) + { + point.x = fingers[fingerId].x; + point.y = fingers[fingerId].y; + get_rect(button, &rect); + + if (SDL_PointInRect(&point, &rect) && button->active) + { + button->down = true; + button->fingerId = fingers[fingerId].id; + fingers[fingerId].on_button = true; + break; + } + } + } + } + + bool button_tapped(TouchButtonID button) + { + if (key.using_touch && buttons[button].active && buttons[button].down) + { + for (int i = 0; i < fingers.size(); i++) + { + if (fingers[i].id == buttons[button].fingerId) + { + return fingers[i].pressed; + } + } + } + return false; + } + + bool touching_right(void) + { + int width; + int height; + gameScreen.GetScreenSize(&width, &height); + + for (int i = 0; i < fingers.size(); i++) + { + if (fingers[i].on_button) + { + continue; + } + + if (fingers[i].x > width / 2) + { + return true; + } + } + return false; + } + + bool screen_tapped(void) + { + for (int i = 0; i < fingers.size(); i++) + { + if (fingers[i].on_button) + { + continue; + } + + return fingers[i].pressed; + } + return false; + } +} diff --git a/desktop_version/src/Touch.h b/desktop_version/src/Touch.h new file mode 100644 index 00000000..ea1bb427 --- /dev/null +++ b/desktop_version/src/Touch.h @@ -0,0 +1,63 @@ +#ifndef TOUCH_H +#define TOUCH_H + +#include + +#include + +struct VVV_Finger +{ + float x; + float y; + bool pressed; + bool on_button; + SDL_FingerID id; +}; + +enum TouchButtonID +{ + /* General */ + TOUCH_BUTTON_LEFT, + TOUCH_BUTTON_RIGHT, + + /* Gameplay */ + TOUCH_BUTTON_MAP, + + /* Menus */ + TOUCH_BUTTON_CANCEL, + TOUCH_BUTTON_CONFIRM, + + NUM_TOUCH_BUTTONS +}; + +struct TouchButton +{ + int x; + int y; + int width; + int height; + bool down; + bool active; + SDL_Texture* image; + SDL_FingerID fingerId; +}; + +namespace touch +{ + extern std::vector fingers; + extern TouchButton buttons[NUM_TOUCH_BUTTONS]; + extern float scale; + + void refresh_buttons(void); + void reset(void); + void update_buttons(void); + + void init(void); + void render(void); + + bool button_tapped(TouchButtonID button); + bool touching_right(void); + bool screen_tapped(void); +} + +#endif /* TOUCH_H */ diff --git a/desktop_version/src/main.cpp b/desktop_version/src/main.cpp index 1dc9f989..6da8aea4 100644 --- a/desktop_version/src/main.cpp +++ b/desktop_version/src/main.cpp @@ -33,6 +33,7 @@ #include "RenderFixed.h" #include "Screen.h" #include "Script.h" +#include "Touch.h" #include "UtilityClass.h" #include "Vlogging.h" @@ -527,6 +528,10 @@ int main(int argc, char *argv[]) { loc::show_translator_menu = true; } + else if (ARG("-emutouch")) + { + SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "1"); + } #ifdef _WIN32 else if (ARG("-console")) { @@ -614,6 +619,9 @@ int main(int argc, char *argv[]) // Set up screen graphics.init(); + // Set up touch input before we load settings + touch::init(); + game.init(); game.seed_use_sdl_getticks = seed_use_sdl_getticks; @@ -813,6 +821,11 @@ int main(int argc, char *argv[]) key.isActive = true; + if (SDL_GetNumTouchDevices() > 0) + { + key.using_touch = true; + } + gamestate_funcs = get_gamestate_funcs(game.gamestate, &num_gamestate_funcs); loop_assign_active_funcs(); diff --git a/desktop_version/src/preloader.cpp b/desktop_version/src/preloader.cpp index 4b47ef99..0e42cb94 100644 --- a/desktop_version/src/preloader.cpp +++ b/desktop_version/src/preloader.cpp @@ -7,6 +7,7 @@ #include "KeyPoll.h" #include "Localization.h" #include "Maths.h" +#include "Touch.h" #include "UtilityClass.h" #include "VFormat.h" @@ -23,7 +24,7 @@ void preloaderinput(void) { game.press_action = false; - if (key.isDown(KEYBOARD_z) || key.isDown(KEYBOARD_SPACE) || key.isDown(KEYBOARD_v) || key.isDown(game.controllerButton_flip)) { + if (key.isDown(KEYBOARD_z) || key.isDown(KEYBOARD_SPACE) || key.isDown(KEYBOARD_v) || key.isDown(game.controllerButton_flip) || touch::screen_tapped()) { game.press_action = true; } diff --git a/desktop_version/touch/buttons/button_left.png b/desktop_version/touch/buttons/button_left.png new file mode 100644 index 0000000000000000000000000000000000000000..a9510ed847041827e86fc4703a7948440cd5a89e GIT binary patch literal 387 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU1|)m_?Z^dE+9j?LCC){ui6xo&c?uz!xv31n zi6shV3jWC@3PuKoMhb?ORt6SUCdLZ720(Du_lpWp8+(bTuPggqMlmryW8rz?KY&7< z1s;*b3=DDyL74GyW=JqlkR{#G*O7r?V?XzwL{=dGwx^3@NX4zUGY;~$81S$#F6Fjr zP(K>TeDtoc)dGHz&ht2yZnO9g56b;w{(}T-c&TF-&-Ti>8@b#zbUJ(q@1z-A>$xF)5Teoamt$# zvjtxyk{Fe)y?tUhnM=HRS>I|M_KDVVMhaEdW&15VZaqw4UZl8r_YLQNtB+s$utv(o z;$)ss<_V=(9nP7t%^D|upLN_aN%@*p;fni?UoN>ZU9t1{WF)__^1%F#t=W20D%s=r bUb@CONn{1`ISV`@iy0WW`9YX*(og24K*1HBE{-7)?pLQc@--;%u&nIA^k4pK@C;uI z0l^%t)>NT2?330_4(l|0*Vv*P%EiJjW?hrU&@Ag`oUlzc&NyM&+_=PZA*M1b#Y7St z6a@~2%uX?R|8<_E$?CcOafdrjOpVihaC*Kw>s7PR!+*9UA3EHV8F%2d*;5U3ZeG*P z7x!E@?h&h&FnG1zXREK4aYD;n)3Xfu)~{zV&MUiS#=PgvntZm38;VB4hj@2?b8^|$ S+jtG=W(H4JKbLh*2~7ZvGAo_&A;DWv+(my_jq2J^Ux}z`N@+Vj3T{`qGIRHI zsc^GcfIIR|nt^ni=>;>@u%iL$uD5wuKGoJ5sCvkCF*G;e!NA83Akz1DHr}s4b c`rg{b@Wj>F_3W1upnzuZboFyt=akR{0G3mfWdHyG literal 0 HcmV?d00001 diff --git a/desktop_version/touch/tutorial/arrowleft.png b/desktop_version/touch/tutorial/arrowleft.png new file mode 100644 index 0000000000000000000000000000000000000000..a42d1d3d1f5846d0d256fc0ad702a5dabe306a1e GIT binary patch literal 259 zcmeAS@N?(olHy`uVBq!ia0vp^d>}Rl8<3oNC%zs?X_vT0lsFfqCYEI8=P86_=B6@) zWRxmcDEKB;Di|3UnkX2WSs7be0U5dmK=AL6Boj~@dx@v7EBifG4t{kBZAaNWpb%$) zM`SSr1Gf+eGhVt|_XjA*lJ4m1$iT3%pZiZDE07=U>EaktaVt6H0MkEa#-6hrEH|zg zJiOe(zhS~E%_+AfuDT`VIu&Z!`ToDiaOeG!TP?-~8S5raoFIR=!YANS;nnHJ{~ww? v>PlH-=Ex|#iQ%N5E8B~6YE~x&vyB;SCYsDpDi?48TEpP!>gTe~DWM4f|I<+I literal 0 HcmV?d00001 diff --git a/desktop_version/touch/tutorial/arrowright.png b/desktop_version/touch/tutorial/arrowright.png new file mode 100644 index 0000000000000000000000000000000000000000..b03ea44f907dd7a48d9fccf4fa8390d9f8d9ac77 GIT binary patch literal 267 zcmeAS@N?(olHy`uVBq!ia0vp^d>}Rl8<3oNC%zs?X_vT0lsFfqCYEI8=P86_=B6@) zWRxmcDEKB;Di|3UnkX2WSs7be0U5dmK=AL6Boj~@dx@v7EBifG4t{l|x4uX20EIXU zJR*x37`TN%nDNrxx<5ccmUKs7M+SzC{oH>NS%LgyPZ!6Kid)Gk2N?e`GxnV2V0rP$ zhsT8>#rc7h^UhvLhZ!MfU2b;x8gtt{e{hNCldtFl^t8}=M$QC$~SYuS96 zTS4E*(1~%Q*}*H5o`j@u7u;%*yY*t7(kZu`I7SAZW>q&g<2zzN>li#;{an^LB{Ts5 Dib+x? literal 0 HcmV?d00001 diff --git a/desktop_version/touch/tutorial/lefthand_far.png b/desktop_version/touch/tutorial/lefthand_far.png new file mode 100644 index 0000000000000000000000000000000000000000..cb7b05f29dd689b2721b277ea104b4c1cbbaf431 GIT binary patch literal 681 zcmeAS@N?(olHy`uVBq!ia0vp^?m!&F!3HF47Ontsv`btgN}P*Q6H7Al^Atidb5j{Y zGD;OJ6nqma6^slFO%x1Gtqe@9j7$}D4S?Wr-O5V0FP6LHF z3p^r=85p>QK$!8;-MT+OL6&q!Uq=Rpjs4tz5?L7-m>4}>978H@y`7O5bjU!!&G7Np zFE0O{xKufmCUz~IrhhWc`O@9@XD#@$Chn{CWnLcaZnw{Ff3;nGe$aY#yIm3syJh&3 zSc)yMT_b3|_^#>f=}?%Oo=a1n!f@Wt0Rqwh!XJy1)3V*M;K(Q^6m z#3hsLySPp>zL_TU@?7gWv1!aVrXJq<-0MYAk)X=kMnR5PzrGyVxc4!t;cTZFQ=YoS zRXa@!w)&uC8Tgtr(oZwgwPHu{)fiSC_5fA&TepN)&AIh3>~v>{|B9ll!0>`C*3*|& zOk)l>-MQggi-x=P2{p@muQ>zz#3r9SU-LX`nK2IuQt7E2LwlcY16tz&MA=*;x z^#ULfz<1z_n9b?53t8C*ix{RbFOXq)%AmpSV99WbF@UcDMW{i(tn{eEmpiegz{J7e M>FVdQ&MBb@0JdQnzW@LL literal 0 HcmV?d00001 diff --git a/desktop_version/touch/tutorial/lefthand_near.png b/desktop_version/touch/tutorial/lefthand_near.png new file mode 100644 index 0000000000000000000000000000000000000000..7c941647e321ff4eebc963a8d5f25808c77edeb0 GIT binary patch literal 720 zcmV;>0x$iEP)WdKxYbs#t(O<{Q;GB7bTATc#MFg7|dF(554FfcHA=V#{t000McNliru z-USE_1`{#PyD|U(010qNS#tmY3labT3lag+-G2N4000DMK}|sb0I`n?{9y$E00JmU zL_t(|+U=WNO2jY}Ms4xUg}4CMp>KkV5kzn?f^WJG7vMsCR&QZCElt14&5x7Z`yCJz zw2r4IX;U-9MMWMz-haLxo`=KNZ{MFae_e4dOC03>>Sq4@AV9>N`;W_ItZ`5aVua#y ztcVekhn3Z#w4~UabN%`h6w})zyrjsm+7r_|s&bY?>3~%+LRLAJ#R$oAtcwv&4l8d% zX^FZVFJc6h98Y3|XgS`*2&*|KG`|QQ#Rzjb6tkts@}VV8t?yoL?}pf8P32grCT8KQ zSsYh+o@0g4Q#IP}WjTPLoZhOb1S*?K*^S^=`ti{9 zB62#YCkgB2%qQ*d##Q~p*5zbDIRMLYvWApkSxz>T11Kh^vCDb(kP=u9fN}uMy=$g4 zIgeRb4uEn1lmnm~0ObHE2S7Oh$^lRgfN}to1Na3F-lu;Fb!))@0000#*d(IEpKSH;V3 zFN%KMteqw95ttd|bf`DkdzBHQ#0b#h{=d%V{|$r@wc1+6cH>`FY#@YtxyyvdRZb=fth5T=1(df-fP|ul;II$ldVIMMip8ud?VqDT(ndoi%;3 z=Tu>f$(L8nl9Lm?X0YG6PGp_V!9T|ewAwePolM!cZO+y%qwgCc++W|!%20f!VN$IX zKF2j}bNY*wv+k{nI(z>)2bcPZCG!^FV+lJrS?24<2cCUa&iT$8)Fb#Fg!e733VF_V zn$f37=IUa$(~M`HY?x;$cREn|6u0G?=koneS8UX3|9U6UvSE5r1pnzq2g{4EfgE4U zi6BbFG7vNn~YUVEp9i;uunK>+S4Fzrzj!E|WE` zHw%9}+4-Ph0pF%461&r0&OEbY^*wbiwYuQDQ(a^9)So^6{d)QHulbAfFL?VqE}x$K zMM%V{@1k>ngr!@y-YToRh-*0j6 zF|u8J?B~QeGvitN6D)gHJ<$<=p;_AYbp71o2HA6^znd?0Z}Bgs0Co3!{^)wB6MGyOkJjR@FM%&)}do?T>PTrhQ6Ta@+FXxky8+luwFg*BS zu=UzKt-vCNNyUuu?AQMYaeq1K$hcs7^9iZhE7neplJ??j2=nv1nQ}5QSNGJDCvs*> z=frd!Q+3Lox`soQ-9dHvmWZw=8@7u17G?2GbMxa3O)lDYZB;D07oWl&8UN0ZlZlyY zFP!`+my}@{7n7Erw^nTN`X>xdr!qq&^eit%Yo`=NDOxgYpL1gI(kBcm>;muRTSfKl uoWwjqrUArP0J@pcgUqif~xvPQ-DI8 z1s;*b3=G^tAk28_ZrvZCAWOQVuOkD)#(wTUiL49^jORRE978H@y}gy~*Ww`HnrM>e zAO6HYdtO&Rrw9ujT`g&ljpLE zFdesI*5K(gmC_LDGZoU1=`-chQ0X&e($ML<$}J*w{1qctfAUtQr3W@fHC%cSvDIN^ z!5XOrB^J}fHB|eaGFD0dWZr+zK5pH=3(Cd%$CYcponC%X_V2#Td~JWdIkQ|{qWDh> zHg0~io?S-JqJP^p=01)&i@sK^nl~Z+g}uCJ#JN>M|HV%4e06Zr)0{7RBrfZvx7j;K zobxLyTo!eXCtEYwQtA5a-Sf1ozLXVg+d5^V%*B_dcV=&^n5N8>zWDif6$_<;u&K+n zfmGME^C=HH4peUWs=^PnB=!G$`vRAjg;%1RzgVogV&VRNR?uUasba@J|L@kDlkuP9 zrS8+BJ>u4?ecM;vRz0-)$*lDp)0DkhgR3`$JWR3PdP!&b(?iP+X0Ci`w)6JNtG`MQ z6uCSstm2!fHb-Z9*o&uM;*MOf*jsw?TuoN*j<1n@8Y<^jFfZm-|Fu%I$Z|20`;}hd zpVyKsl?q%A=XFNPak$@mrsK}i#{mompcv4hf{n^d$O8WuEZZmi{&!_jH87?bJYD@< J);T3K0RTy_jG*M4RDW+wFFo5dxK z1p*eAI2IUKOfn77kU1%&A=8(_Dk9{*iGizI`72ZFv58d;haP*Zb&+#CcErqj=eympgefv=O+|?%)J3ic=Jl8nI zQ106<*L7;gqn}L{PR&yBR(Y>@IVHk8=k;YUn6_@#pJN z9`#cve@Li5k9_H^U+8e;X54)(MLrE%zFh~W$+1tBI{r2}zg$DN?^#-1l#H)t;TC(w zrAIDK<6Hi${cwqe>C!_h|L(LD^~=pHh>{EPXucf8HRqapbkKt>Vsgu$H?H_#VcUA_ zWo?h1iC=zVp^n@>p6=v851G~OWzLTxTxA4+HZlsbyT}As$egaf*K&AckA(uzWz0%I ehhs7p)F+>xwWGgY*&LVz7(8A5T-G@yGywpUr3(T8 literal 0 HcmV?d00001 diff --git a/desktop_version/touch/tutorial/touchscreen.png b/desktop_version/touch/tutorial/touchscreen.png new file mode 100644 index 0000000000000000000000000000000000000000..43578e7aae876b39ad5e9eff331931a86b970397 GIT binary patch literal 656 zcmeAS@N?(olHy`uVBq!ia0vp^3xGI+gAGU)@%@PeQrac15hcz=sfi_-`FRQ#9xDg0GT)BcouNP> z&H|6fVg?4eLmg&B)b1Z=02M{-2FMUSzG-EO%H`CwuVf%jdnn z>R;bI?tWZeZ4cv_^E^GOGm7_u zp;vyi@7BFHWR8ct`TF!|!Q%RxbaUbv