diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 5cd2e656a4..449db31136 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -398,6 +398,7 @@ list(APPEND SOURCE_FILES displayapp/screens/Alarm.cpp displayapp/screens/Styles.cpp displayapp/screens/WeatherSymbols.cpp + displayapp/screens/Magic8ball.cpp displayapp/Colors.cpp displayapp/widgets/Counter.cpp displayapp/widgets/PageIndicator.cpp diff --git a/src/displayapp/DisplayApp.cpp b/src/displayapp/DisplayApp.cpp index bfd7dbed6d..4ff3a45d6d 100644 --- a/src/displayapp/DisplayApp.cpp +++ b/src/displayapp/DisplayApp.cpp @@ -31,6 +31,7 @@ #include "displayapp/screens/PassKey.h" #include "displayapp/screens/Error.h" #include "displayapp/screens/Calculator.h" +#include "displayapp/screens/Magic8ball.h" #include "drivers/Cst816s.h" #include "drivers/St7789.h" diff --git a/src/displayapp/UserApps.h b/src/displayapp/UserApps.h index 25926edc40..2ca7d666ca 100644 --- a/src/displayapp/UserApps.h +++ b/src/displayapp/UserApps.h @@ -7,6 +7,7 @@ #include "displayapp/screens/Timer.h" #include "displayapp/screens/Twos.h" #include "displayapp/screens/Tile.h" +#include "displayapp/screens/Magic8ball.h" #include "displayapp/screens/ApplicationList.h" #include "displayapp/screens/WatchFaceDigital.h" #include "displayapp/screens/WatchFaceAnalog.h" diff --git a/src/displayapp/apps/Apps.h.in b/src/displayapp/apps/Apps.h.in index f6feeb7b6d..15e33edd86 100644 --- a/src/displayapp/apps/Apps.h.in +++ b/src/displayapp/apps/Apps.h.in @@ -43,7 +43,8 @@ namespace Pinetime { SettingChimes, SettingShakeThreshold, SettingBluetooth, - Error + Error, + Magic8ball, }; enum class WatchFace : uint8_t { diff --git a/src/displayapp/apps/CMakeLists.txt b/src/displayapp/apps/CMakeLists.txt index 93196ed6a0..ff0559d8ee 100644 --- a/src/displayapp/apps/CMakeLists.txt +++ b/src/displayapp/apps/CMakeLists.txt @@ -16,6 +16,7 @@ else () set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Calculator") set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Weather") #set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Motion") + set(DEFAULT_USER_APP_TYPES "${DEFAULT_USER_APP_TYPES}, Apps::Magic8ball") set(USERAPP_TYPES "${DEFAULT_USER_APP_TYPES}" CACHE STRING "List of user apps to build into the firmware") endif () diff --git a/src/displayapp/screens/Magic8ball.cpp b/src/displayapp/screens/Magic8ball.cpp new file mode 100644 index 0000000000..d6a1f3e6cb --- /dev/null +++ b/src/displayapp/screens/Magic8ball.cpp @@ -0,0 +1,297 @@ +#include "displayapp/screens/Magic8ball.h" + +// How big the 3d acceleration vector needs to be to be taken as a shake +#define SHAKE_THRESHOLD 2048 +// If two taps are gotten within this many ticks, consider it a double tap and refresh the response +#define DOUBLE_TAP_TIME_TICKS pdMS_TO_TICKS(500) + +// After a shake is taken, how long the screen should stay black before starting to fade in (can be set to 0) +#define FADEIN_PAUSE_TICKS pdMS_TO_TICKS(200) +// Once pause is finished, fade in (can be set to 0) +#define FADEIN_FADE_TICKS pdMS_TO_TICKS(300) +// Smooth fading (or as good as the watch can do) looks bad. Fade in with this many discrete steps. +// (ex: pause=100ms, fade=150ms, steps=4 -> 0ms: 0%, 100ms: 25%, 150ms: 50%, 200ms: 75%, 250ms: 100%, no interpolation.) +#define FADEIN_STEP_COUNT 3 + +using namespace Pinetime::Applications::Screens; + +namespace { + void EventHandler(lv_obj_t* obj, lv_event_t event) { + auto* screen = static_cast(obj->user_data); + screen->UpdateSelected(obj, event); + } +} + +Magic8ball::Magic8ball(Controllers::MotionController& motionController) : motionController {motionController} { + + activePool = 0; // default pool + isMenuOpen = false; + fadeStartTimestamp = 0; + lastClickTimestamp = xTaskGetTickCount() - DOUBLE_TAP_TIME_TICKS; + triangleDisplayState = Displaying; + lastTriangleFadeColor = LV_COLOR_BLUE; + std::srand(xTaskGetTickCount()); + + // Outline for the big blue background triangle + lv_style_init(&bigBlueTriangleOutlineStyle); + lv_style_set_line_width(&bigBlueTriangleOutlineStyle, LV_STATE_DEFAULT, 16); + lv_style_set_line_color(&bigBlueTriangleOutlineStyle, LV_STATE_DEFAULT, LV_COLOR_BLUE); + lv_style_set_line_rounded(&bigBlueTriangleOutlineStyle, LV_STATE_DEFAULT, true); + bigBlueTriangleOutline = lv_line_create(lv_scr_act(), nullptr); + lv_line_set_points(bigBlueTriangleOutline, triangleOutlinePoints, 4); + lv_obj_add_style(bigBlueTriangleOutline, LV_LINE_PART_MAIN, &bigBlueTriangleOutlineStyle); + + // Fill for the big blue background triangle + // Consists of a mask with two lines matching the bottom left and bottom right slopes, and a child basic object + // which matches the size of the background triangle. + lv_obj_t* bigBlueTriangleFillMask = lv_objmask_create(lv_scr_act(), nullptr); + lv_obj_set_size(bigBlueTriangleFillMask, LV_HOR_RES, LV_VER_RES); + lv_obj_align(bigBlueTriangleFillMask, nullptr, LV_ALIGN_CENTER, 0, 0); + lv_draw_mask_line_param_t bigBlueTriangleFillMaskLineL, bigBlueTriangleFillMaskLineR; + lv_draw_mask_line_points_init(&bigBlueTriangleFillMaskLineL, + triangleOutlinePoints[0].x, + triangleOutlinePoints[0].y, + triangleOutlinePoints[2].x, + triangleOutlinePoints[2].y, + LV_DRAW_MASK_LINE_SIDE_RIGHT); + lv_draw_mask_line_points_init(&bigBlueTriangleFillMaskLineR, + triangleOutlinePoints[1].x, + triangleOutlinePoints[1].y, + triangleOutlinePoints[2].x, + triangleOutlinePoints[2].y, + LV_DRAW_MASK_LINE_SIDE_LEFT); + lv_objmask_add_mask(bigBlueTriangleFillMask, &bigBlueTriangleFillMaskLineL); + lv_objmask_add_mask(bigBlueTriangleFillMask, &bigBlueTriangleFillMaskLineR); + lv_style_init(&bigBlueTriangleFillStyle); + lv_style_set_radius(&bigBlueTriangleFillStyle, LV_STATE_DEFAULT, 0); + bigBlueTriangleFill = lv_obj_create(bigBlueTriangleFillMask, nullptr); + lv_obj_set_size(bigBlueTriangleFill, + triangleOutlinePoints[1].x - triangleOutlinePoints[0].x, + triangleOutlinePoints[2].y - triangleOutlinePoints[0].y); + lv_obj_set_style_local_bg_color(bigBlueTriangleFill, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, LV_COLOR_BLUE); + lv_obj_add_style(bigBlueTriangleFill, LV_LINE_PART_MAIN, &bigBlueTriangleFillStyle); + lv_obj_set_pos(bigBlueTriangleFill, triangleOutlinePoints[0].x, triangleOutlinePoints[0].y); + + // Main text (centered 5/8 up from the bottom of the triangle) + mainText = lv_label_create(lv_scr_act(), nullptr); + lv_label_set_text_static(mainText, helpText); + lv_label_set_align(mainText, LV_LABEL_ALIGN_CENTER); + lv_obj_align(mainText, bigBlueTriangleFill, LV_ALIGN_CENTER, 0, -(triangleOutlinePoints[2].y - triangleOutlinePoints[0].y) / 8); + + // Button to close long tap menu + btnCloseMenu = lv_btn_create(lv_scr_act(), nullptr); + btnCloseMenu->user_data = this; + lv_obj_set_size(btnCloseMenu, 60, 60); + lv_obj_align(btnCloseMenu, lv_scr_act(), LV_ALIGN_IN_TOP_LEFT, 15, 15); + lv_obj_set_style_local_bg_opa(btnCloseMenu, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_70); + lv_obj_t* lblClose = lv_label_create(btnCloseMenu, nullptr); + lv_label_set_text_static(lblClose, "X"); + lv_obj_set_event_cb(btnCloseMenu, EventHandler); + lv_obj_set_hidden(btnCloseMenu, true); + + // Button to go to next pool in long tap menu + btnPoolNext = lv_btn_create(lv_scr_act(), nullptr); + btnPoolNext->user_data = this; + lv_obj_set_size(btnPoolNext, 60, 60); + lv_obj_align(btnPoolNext, lv_scr_act(), LV_ALIGN_IN_RIGHT_MID, -15, 0); + lv_obj_set_style_local_bg_opa(btnPoolNext, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_70); + lv_obj_t* lblNext = lv_label_create(btnPoolNext, nullptr); + lv_label_set_text_static(lblNext, ">"); + lv_obj_set_event_cb(btnPoolNext, EventHandler); + lv_obj_set_hidden(btnPoolNext, true); + + // Button to go to previous pool in long tap menu + btnPoolPrev = lv_btn_create(lv_scr_act(), nullptr); + btnPoolPrev->user_data = this; + lv_obj_set_size(btnPoolPrev, 60, 60); + lv_obj_align(btnPoolPrev, lv_scr_act(), LV_ALIGN_IN_LEFT_MID, 15, 0); + lv_obj_set_style_local_bg_opa(btnPoolPrev, LV_BTN_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_70); + lv_obj_t* lblPrev = lv_label_create(btnPoolPrev, nullptr); + lv_label_set_text_static(lblPrev, "<"); + lv_obj_set_event_cb(btnPoolPrev, EventHandler); + lv_obj_set_hidden(btnPoolPrev, true); + + // Background for current pool text in long tap menu + currentPoolBG = lv_obj_create(lv_scr_act(), nullptr); + lv_obj_set_size(currentPoolBG, 210, 60); + lv_obj_align(currentPoolBG, lv_scr_act(), LV_ALIGN_IN_BOTTOM_MID, 0, -15); + lv_obj_set_style_local_bg_opa(currentPoolBG, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, LV_OPA_70); + // Copy the color from whatever the default color for a button is + lv_obj_set_style_local_bg_color(currentPoolBG, + LV_OBJ_PART_MAIN, + LV_STATE_DEFAULT, + lv_obj_get_style_bg_color(btnCloseMenu, LV_BTN_PART_MAIN)); + + // Current pool text in long tap menu + currentPoolText = lv_label_create(currentPoolBG, nullptr); + lv_label_set_text_static(currentPoolText, answerPools[activePool].GetName()); + lv_label_set_align(currentPoolText, LV_LABEL_ALIGN_CENTER); + lv_obj_align(currentPoolText, nullptr, LV_ALIGN_CENTER, 0, 0); + lv_obj_set_hidden(currentPoolBG, true); + + taskRefresh = lv_task_create(RefreshTaskCallback, LV_DISP_DEF_REFR_PERIOD, LV_TASK_PRIO_MID, this); + Refresh(); +} + +Magic8ball::~Magic8ball() { + lv_style_reset(&bigBlueTriangleOutlineStyle); + lv_style_reset(&bigBlueTriangleFillStyle); + lv_obj_clean(lv_scr_act()); + lv_task_del(taskRefresh); +} + +bool Magic8ball::OnTouchEvent(Pinetime::Applications::TouchEvents event) { + switch (event) { + case TouchEvents::Tap: + if (isMenuOpen) { + return false; + } + if (xTaskGetTickCount() - lastClickTimestamp < DOUBLE_TAP_TIME_TICKS) { + Start8BallRefresh(); + } + lastClickTimestamp = xTaskGetTickCount(); + return true; + case TouchEvents::LongTap: + if (!isMenuOpen) { + OpenMenu(); + return true; + } + return false; + default: + return false; + } +} + +bool Magic8ball::OnButtonPushed() { + if (isMenuOpen) { + CloseMenu(); + return true; + } + return false; +} + +void Magic8ball::Refresh() { + if (triangleDisplayState == Displaying) { + // Disable shaking if not fading in and menu is open + if (isMenuOpen) { + return; + } + // Find 3d pythagorean theorem for actual total acceleration + lv_sqrt_res_t sqrtResult; + _lv_sqrt(_lv_pow(motionController.X(), 2) + _lv_pow(motionController.Y(), 2), &sqrtResult, 0x8000); + _lv_sqrt(_lv_pow(motionController.Z(), 2) + _lv_pow(sqrtResult.i, 2), &sqrtResult, 0x8000); + if (sqrtResult.i > SHAKE_THRESHOLD) { + Start8BallRefresh(); + // For screen timeout purposes, act as though shaking the watch was a user click + lv_disp_trig_activity(nullptr); + } + } else if (triangleDisplayState == FadingIn) { + const uint32_t fadeProgression = xTaskGetTickCount() - fadeStartTimestamp; + + lv_color_t newColor; + if (fadeProgression < FADEIN_PAUSE_TICKS) { + // In pause period + newColor = LV_COLOR_BLACK; + } else if (fadeProgression < FADEIN_PAUSE_TICKS + FADEIN_FADE_TICKS) { + // In stepping period, do interpolation between black and blue + // stepProgress is integer in range [1,FADEIN_STEP_COUNT) + uint32_t stepProgress = 1 + ((fadeProgression - FADEIN_PAUSE_TICKS) * (FADEIN_STEP_COUNT - 1) / FADEIN_FADE_TICKS); + newColor = LV_COLOR_MAKE(0, 0, 255 * stepProgress / FADEIN_STEP_COUNT); + } else { + // Fade completed + newColor = LV_COLOR_BLUE; + triangleDisplayState = Displaying; + lv_obj_set_hidden(mainText, false); + lv_label_set_text_static(mainText, getRandomString()); + lv_obj_align(mainText, bigBlueTriangleFill, LV_ALIGN_CENTER, 0, -(triangleOutlinePoints[2].y - triangleOutlinePoints[0].y) / 8); + } + + // Commit the new color to the triangle if it's been changed + if (newColor.full != lastTriangleFadeColor.full) { + lastTriangleFadeColor = newColor; + lv_obj_set_style_local_bg_color(bigBlueTriangleFill, LV_OBJ_PART_MAIN, LV_STATE_DEFAULT, newColor); + lv_style_set_line_color(&bigBlueTriangleOutlineStyle, LV_OBJ_PART_MAIN, newColor); + lv_obj_refresh_style(bigBlueTriangleOutline, LV_OBJ_PART_MAIN, LV_STYLE_LINE_COLOR); + } + } +} + +const char* Magic8ball::getRandomString() const { + // Get sum of weights in current pool + uint16_t poolTotalWeight = 0; + const Pool* selectedPool {&answerPools[activePool]}; + for (size_t i = 0; i < selectedPool->size(); i++) { + poolTotalWeight += (*selectedPool)[i].weight; + } + + // Have total weights in the pool, choose something within that now + uint16_t selectedIndex = std::rand() % poolTotalWeight; + CategoryType selectedCategory = None; + for (size_t i = 0; i < selectedPool->size(); i++) { + if (selectedIndex < (*selectedPool)[i].weight) { + selectedCategory = (*selectedPool)[i].categoryType; + break; + } + selectedIndex -= (*selectedPool)[i].weight; + } + + // Now need to randomly pick within selectedCategory + return categories.at(selectedCategory)[std::rand() % categories.at(selectedCategory).size()]; +} + +void Magic8ball::UpdateSelected(lv_obj_t* object, lv_event_t event) { + if (event == LV_EVENT_CLICKED) { + if (object == btnCloseMenu) { + CloseMenu(); + } + if (object == btnPoolNext) { + activePool++; + if (activePool >= static_cast(answerPools.size())) { + activePool = 0; + } + lv_label_set_text_static(currentPoolText, answerPools[activePool].GetName()); + lv_obj_align(currentPoolText, nullptr, LV_ALIGN_CENTER, 0, 0); + } + if (object == btnPoolPrev) { + activePool--; + if (activePool < 0) { + activePool = answerPools.size() - 1; + } + lv_label_set_text_static(currentPoolText, answerPools[activePool].GetName()); + lv_obj_align(currentPoolText, nullptr, LV_ALIGN_CENTER, 0, 0); + } + } +} + +void Magic8ball::OpenMenu() { + if (isMenuOpen) { + return; + } + isMenuOpen = true; + lv_obj_set_hidden(btnCloseMenu, false); + lv_obj_set_hidden(btnPoolNext, false); + lv_obj_set_hidden(btnPoolPrev, false); + lv_obj_set_hidden(currentPoolBG, false); +} + +void Magic8ball::CloseMenu() { + if (!isMenuOpen) { + return; + } + isMenuOpen = false; + lv_obj_set_hidden(btnCloseMenu, true); + lv_obj_set_hidden(btnPoolNext, true); + lv_obj_set_hidden(btnPoolPrev, true); + lv_obj_set_hidden(currentPoolBG, true); +} + +void Magic8ball::Start8BallRefresh() { + // Extra safety, calling code should take care of this though + if (isMenuOpen || triangleDisplayState == FadingIn) { + return; + } + // Set state to fading in + triangleDisplayState = FadingIn; + fadeStartTimestamp = xTaskGetTickCount(); + lv_obj_set_hidden(mainText, true); +} \ No newline at end of file diff --git a/src/displayapp/screens/Magic8ball.h b/src/displayapp/screens/Magic8ball.h new file mode 100644 index 0000000000..464a9d36a2 --- /dev/null +++ b/src/displayapp/screens/Magic8ball.h @@ -0,0 +1,421 @@ +#pragma once + +#include "displayapp/apps/Apps.h" +#include "displayapp/screens/Screen.h" +#include "displayapp/Controllers.h" +#include "components/motion/MotionController.h" +#include "task.h" + +#include +#include +#include +#include + +namespace Pinetime { + namespace Applications { + + namespace Screens { + + class Magic8ball : public Screen { + public: + Magic8ball(Controllers::MotionController& motionController); + ~Magic8ball() override; + + /// Open response type menu on long click + bool OnTouchEvent(TouchEvents event) override; + + /// Function used for button callbacks + void UpdateSelected(lv_obj_t* object, lv_event_t event); + + /// Closes menu if it's open + bool OnButtonPushed() override; + + /// Checks accelerometer value and updates screen if needed + void Refresh() override; + + private: + void OpenMenu(); + void CloseMenu(); + + /// Set state and whatever else is needed for the watch to start the refresh cycle. Does nothing if already fading in. + void Start8BallRefresh(); + + /// Simply choose a random string from the currently selected pool and return it + [[nodiscard]] const char* getRandomString() const; + + Controllers::MotionController& motionController; + lv_obj_t* mainText; + // Describes the outline of the big triangle. Is an equilateral triangle with edge length 204 at the center of the screen. + // Note that these points are used to draw a 16px thick line, so the actual boundary is 8px out from this + // Main text is placed 5/8 way up on the triangle for better spacing. + static constexpr lv_point_t triangleOutlinePoints[4] = {{18, 32}, {222, 32}, {120, 200}, {18, 32}}; + lv_obj_t* bigBlueTriangleOutline; + lv_style_t bigBlueTriangleOutlineStyle; + lv_obj_t* bigBlueTriangleFill; + lv_style_t bigBlueTriangleFillStyle; + lv_obj_t* btnPoolNext; + lv_obj_t* btnPoolPrev; + lv_obj_t* btnCloseMenu; + lv_obj_t* currentPoolText; + lv_obj_t* currentPoolBG; + bool isMenuOpen; + int activePool; + lv_task_t* taskRefresh; + uint32_t fadeStartTimestamp; + lv_color_t lastTriangleFadeColor; + uint32_t lastClickTimestamp; + + enum { Displaying, FadingIn } triangleDisplayState; + + /// Text shown on starting the application + constexpr static char helpText[] = "Ask your quest-\nion and shake\nthe watch\nto get\nan ans-\nwer"; + + /// Represents an entry in categoryContents. Each value is a unique category. + enum CategoryType { + None = 0, // Dummy category containing a single error string as an option + Positive, // Original positive answers + Deferring, // Original deferring answers (like "Ask again") + Negative, // Original negative answers + PositiveExtra, // Custom positive answers + DeferringExtra, // Custom deferring/silly answers (like "Does not compute") + NegativeExtra, // Custom negative answers + Unknown, // Non-answers and silliness + Pawsitive, // Cat themed positive answers + Defurring, // Cat themed deferring answers + Mewgative, // Cat themed negative answers + }; + + /// Entries in Pool objects + /// @param weight The weight in the pool to give this item + /// @param categoryType The category this member is referring to + struct PoolMember { + uint16_t weight; + CategoryType categoryType; + }; + + /// The topmost structure in this app. Defines each option available in the app + class Pool { + public: + /// @param name The name of the category to display + /// @param usedCategories An initializer list of PoolMembers as the pool contents + Pool(const char* name, const std::initializer_list usedCategories) + : name(name), categoryCount(usedCategories.size()) { + categories = std::shared_ptr(new PoolMember[categoryCount]); + std::copy(usedCategories.begin(), usedCategories.end(), categories.get()); + } + + PoolMember& operator[](const size_t index) const { + return categories.get()[index]; + } + + [[nodiscard]] size_t size() const { + return categoryCount; + } + + [[nodiscard]] const char* GetName() const { + return name; + } + + private: + const char* name; + size_t categoryCount; + std::shared_ptr categories; + }; + + // categoryContents and answerPools are not static because I don't want them in memory when not in use and there + // should only ever be at most a single instance of this class anyway. + + // Comfortable text sizing guides: + // 6 rows: + // 123456789ABCDEF\n123456789ABCD\n123456789AB\n123456789\n1234567\n1234 + // 5 rows: + // 123456789ABCDE\n123456789ABC\n123456789A\n12345678\n12345 + // 4 rows: + // 123456789ABCD\n123456789AB\n123456789\n1234567 + // 3 rows: + // 123456789ABC\n123456789A\n12345678 + // 2 rows: + // 123456789AB\n123456789 + // 1 row: + // 123456789A + + /// A map from CategoryType to a vector containing the results in each category. + /// Must map every option in enum CategoryType (or all used ones)! + const std::map> categories = { + {None, + { + "Bug hap-\npened, plz\nreport", + }}, + // Original answers from https://magic-8ball.com/magic-8-ball-answers/ + {Positive, + { + "It is\ncertain", + "Without a\ndoubt", + "It is deci-\ndedly so", + "Yes defi-\nnitely", + "You may\nrely on\nit", + "As I see\nit, yes", + "Most\nlikely", + "Outlook\ngood", + "Yes", + "Signs point\nto yes", + }}, + {Deferring, + { + "Reply hazy,\ntry again", + "Ask again\nlater", + "Better not\ntell you\nnow", + "Cannot pre-\ndict now", + "Concentrate\nand ask\nagain", + }}, + {Negative, + { + "Don't count\non it", + "My reply\nis no", + "My sources\nsay no", + "Outlook\nnot so\ngood", + "Very\ndoubtful", + }}, + {PositiveExtra, + { + "The Wall\nPeople are\nconfi-\ndent", + "Why not?", + "Horse of abso-\nlute agree-\nment and\nunderst-\nand-\ning:", + "oo ye", + // "If that's\nwhat you\nwant", + "Eh,\nprobably", + "Hell yeah", + "Yes, but be\ncareful", + "Yes, but\nwash your\nhands\nafter", + "Not bad\nodds", + "It will end\nfavorably", + "Chances\ncould be\nworse", + "Seems\npossible\nenough", + "With enough\neffort,\nyes", + ":)", + "I see no\nissues\nwith it", + "Disappoint-\ningly,\nyes", + "Most vigor-\nously", + "Probably", + "Yes, and I\nwant to\nwatch", + "No\n-Actually,\nyes", + "Only after\n5pm", + "Oh sweet\nheavens\nyes", + "I think so", + "Outlook\ngood, if\na little\nillegal", + "Yes...?", + "If the\nprophecy is\nto be be-\nlieved", + "As surely\nas 2+2=4", + "Yes!", + "Yes!!!!", + "Yes, and the\nhorse you\nrode in\non", + "Obviously", + "You'll\nprobably\nbe okay", + }}, + {DeferringExtra, + { + // "The Wall\nPeople are-\nn't here\nnow", + "Ask a\ntherapist\ninstead", + "Error 500\n---\nInternal\nserver\nerror", + "You just\nlost the\ngame, try\nagain", + "RTFM and\nask again", + "Try tea\nreading\ninstead", + "Your quest-\nion is imp-\nortant to\nus", + "Again~", + "NOT\nANSWERING\nTHAT", + "Apply glue\nliberally\nand ask\nagain", + "Reevaluate\nyourself\nand ask\nagain", + "Does not\ncompute", + "Out for\nlunch", + "BRB", + }}, + {NegativeExtra, + { + "The Wall\nPeople are\nnot con-\nfident", + "Eeny\nmeeny\nminey\nno", + "Please re-\nconsider", + "Strongly\nconsider an\naltern-\native", + "Hold\n[shift]\nto run", + "For legal\nreasons,\nno", + "Nah", + "Grandma\nwould be\nsad", + // "Only with\n911 on\nspeed\ndial", + "Just move\non", + "Cease the\nthought", + "Chances\nare poor", + "Your confi-\ndence is\nmis-\nplaced", + "Could be\nbetter", + "NO NO NO NO\nNO NO NO\nNO NO", + "Pray first", + "Yes\n-Actually,\nno", + "No, and nip-\nples don't\ngrow\nback", + "Good\nluck...", + "Lower your\nexpectat-\nions", + "Answer is\nnot favor-\nable", + "God no", + "Not really", + "No, that's\nnot legal\nanymore", + "As surely\nas 2+2=5", + "Sorry", + "That's ill-\negal in 30\nstates", + "Fffffor\nyou, no", + "I'd bet\nagainst\nit", + "Do you want\nflattery\nor hon-\nnesty?", + "How about\nno", + }}, + {Unknown, + { + "Answer is\nunclear", + "What?", + "Hard to\ntell", + "I dunno", + "Stop\nshaking\nme!", + "Don't ask\nthat", + "How would\nI know!", + "Google it", + "Bother\nsomeone\nelse", + "But nobody\ncame", + "Apple\njuice\nissues", + "Is your\nfridge\nrunning?", + "Is that\nHatsune\nMiku?", + "Ew", + "explosive\ndiarrhea", + "Wario app-\nroaches", + "so real", + "Go back\nto bed", + "Among us?", + "Allan\nplease add\ndetails", + "I knew you\nwould ask\nthat", + "Your fly\nis down", + "Bruh", + "Oo I know\nthis one!", + "What do you\nthink?", + "Answer DLC\nonly\n$3.99!", + "Do not\nthe cat", + "I know what\nyou did", + "A pickle\nwould\nsuffice", + "CHICKEN\nJOCKEYYY", + "Bring a\ncamera", + "Bees?\nBees", + "Take a\nbrick\nwith you", + "Would your\nmother be\nproud?", + "Remove the\nbones\nfirst", + "Would you\nrather\ndrink hot\nsauce", + "Dream big\nFart loud" + "Gone\nfishing", + "Shh I'm\nhiding", + "Suddenly,\nhiccups", + "Stop\neating\nworms", + }}, + // The cat based ones were written at 5am. They were not meant to be good. Still, somehow I feel the need to apologize. + // I would like to thank my brother for subjecting himself to reading articles of awful cat puns to use as inspiration. + {Pawsitive, + { + "Of course,\nnya~", + "Pawsitively\nso", + "Outlook\npurrfect", + "You have the\nright cati-\ntude for\nit", + "Furry good\noutlook", + "Yes, but\nafter a\nnap", + "Don't be\nfurantic,\nodds are\ngood", + "Nyo way for\nit to not\nbe the\ncase", + "Mew've got\nthis", + "Nyabso-\nlutely", + "Nyes", + "Your future\nis\npurright", + "Affur-\nmative!", + }}, + {Mewgative, + { + "A little\nbirdy told\nme no", + "Nyo", + "A cat-\nastrophe\napproach-\nes", + "Are you feline\nlucky?\nYou shoul-\ndn't\nbe", + "Nyanything\nbut that", + "Your claws\nare sharp\nbut your\nhead is\ndull", + "Fluff no!", + "Nyanfor-\ntunately\nnyat", + "Impawsible", + "Oh,\npaw-lease", + "Meowtlook\nnot good", + }}, + {Defurring, + { + "Anything is\npawsible", + "I'm just a\ncat, what\ndo you\nwant", + "Meow is not\nthe time", + "Food first", + "Ask again,\nnya", + "Answer is\nnyanclear", + "Meow", + }}, + }; + + /// Vector of Pool objects containing information on what to display in the app. + /// On app startup, index 0 is selected. + std::vector answerPools { + {"Default", + { + {10, Positive}, + {5, Negative}, + {5, Deferring}, + {20, PositiveExtra}, + {10, NegativeExtra}, + {10, DeferringExtra}, + {1, Unknown}, // ~1.6% chance for a silly nonsense answer + }}, + {"OG", + { + {10, Positive}, + {5, Negative}, + {5, Deferring}, + }}, + {"Fair", + { + {10, Positive}, + {5, Negative}, + {10, Deferring}, + {20, PositiveExtra}, + {10, NegativeExtra}, + {20, DeferringExtra}, + {1, Unknown}, // ~1.3% chance for a silly nonsense answer + }}, + {"Rig - Yes", + { + {15, Positive}, + {5, Deferring}, + {30, PositiveExtra}, + {10, DeferringExtra}, + }}, + {"Rig - No", + { + {15, Negative}, + {5, Deferring}, + {30, NegativeExtra}, + {10, DeferringExtra}, + }}, + {"Cat", + { + {10, Pawsitive}, + {5, Mewgative}, + {5, Defurring}, + }}, + }; + }; + } + + template <> + struct AppTraits { + static constexpr Apps app = Apps::Magic8ball; + static constexpr const char* icon = "8"; + + static Screens::Screen* Create(AppControllers& controllers) { + return new Screens::Magic8ball(controllers.motionController); + } + + static bool IsAvailable(Pinetime::Controllers::FS& /*filesystem*/) { + return true; + }; + }; + } +} \ No newline at end of file