├── CNAME ├── .github ├── FUNDING.yml ├── no-response.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── favicon.ico ├── img ├── favicon-32.png ├── favicon-128.png ├── favicon-152.png ├── favicon-167.png ├── favicon-180.png ├── favicon-192.png ├── favicon-196.png └── favicon-512.png ├── NOTICE ├── manifest.json ├── COPYRIGHT ├── js ├── contributors.js ├── themes.js ├── translations.js ├── chart.js ├── scripts.js └── predictions.js ├── service-worker.js ├── locales ├── ko.json ├── zh-CN.json ├── zh-TW.json ├── ja.json ├── gl.json ├── th.json ├── nl.json ├── pl.json ├── cs.json ├── it.json ├── ca.json ├── ru.json ├── ua.json ├── id.json ├── de.json ├── hu.json ├── en.json ├── fr.json ├── ph.json ├── es.json └── pt-BR.json ├── README.md ├── LICENSE ├── index.html └── css └── styles.css /CNAME: -------------------------------------------------------------------------------- 1 | turnipprophet.io -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: 2 | - mikebryant 3 | - theRTC204 4 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebryant/ac-nh-turnip-prices/HEAD/favicon.ico -------------------------------------------------------------------------------- /img/favicon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebryant/ac-nh-turnip-prices/HEAD/img/favicon-32.png -------------------------------------------------------------------------------- /img/favicon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebryant/ac-nh-turnip-prices/HEAD/img/favicon-128.png -------------------------------------------------------------------------------- /img/favicon-152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebryant/ac-nh-turnip-prices/HEAD/img/favicon-152.png -------------------------------------------------------------------------------- /img/favicon-167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebryant/ac-nh-turnip-prices/HEAD/img/favicon-167.png -------------------------------------------------------------------------------- /img/favicon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebryant/ac-nh-turnip-prices/HEAD/img/favicon-180.png -------------------------------------------------------------------------------- /img/favicon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebryant/ac-nh-turnip-prices/HEAD/img/favicon-192.png -------------------------------------------------------------------------------- /img/favicon-196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebryant/ac-nh-turnip-prices/HEAD/img/favicon-196.png -------------------------------------------------------------------------------- /img/favicon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikebryant/ac-nh-turnip-prices/HEAD/img/favicon-512.png -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Originally developed by Mike Bryant, with great thanks to Ninji 2 | 3 | Original project location: https://github.com/mikebryant/ac-nh-turnip-prices 4 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Turnip Prophet - ACNH Turnip Tracker", 3 | "short_name": "Turnip Prophet", 4 | "description": "An app to track your Animal Crossing: New Horizons turnip prices daily!", 5 | "start_url": "index.html", 6 | "display": "standalone", 7 | "background_color": "#def2d9", 8 | "theme_color": "#def2d9", 9 | "icons": [ 10 | { 11 | "src": "/img/favicon-192.png", 12 | "sizes": "192x192" 13 | }, 14 | { 15 | "src": "/img/favicon-512.png", 16 | "sizes": "512x512" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.github/no-response.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-no-response - https://github.com/probot/no-response 2 | 3 | daysUntilClose: 7 4 | responseRequiredLabel: more information required 5 | closeComment: > 6 | This issue has been automatically closed because there has been no response 7 | to our request for more information from the original author. With only the 8 | information that is currently in the issue, we don't have enough information 9 | to take action. Please reach out if you have or find the answers we need so 10 | that we can investigate further. 11 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Copyright 2020 Mike Bryant 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not files from this repository except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /js/contributors.js: -------------------------------------------------------------------------------- 1 | function getContributors(page) { 2 | const PER_PAGE = 100 3 | if (window.jQuery) { 4 | const container = $('#contributors'); 5 | jQuery.ajax(`https://api.github.com/repos/mikebryant/ac-nh-turnip-prices/contributors?page=${page}&per_page=${PER_PAGE}`, {}) 6 | .done(function (data) { 7 | const contributorList = []; 8 | data.forEach((contributor, idx) => { 9 | if (idx === 0 && page > 1) { 10 | contributorList.push(', '); 11 | } 12 | 13 | contributorList.push(`${contributor.login}`); 14 | if (idx < data.length - 1) { 15 | contributorList.push(', '); 16 | } 17 | }); 18 | container.append(contributorList.join('')); 19 | // If the length of the data is < PER_PAGE, we know we are processing the last page of data. 20 | if (data.length < PER_PAGE) return; 21 | getContributors(page + 1); 22 | }); 23 | } 24 | } 25 | 26 | $(document).ready(getContributors(1)); 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Things to check** 11 | - Have you forced a refresh in your browser (e.g. ctrl+F5 in Chrome/Firefox)? 12 | - yes/no 13 | - Have you made sure the first time buyer option is correct? If you're unsure, try it both ways 14 | - yes/no 15 | - Have you time-travelled this week? Travelling backwards resets the prices, and therefore you must not put prices in from both before & after the time-travel 16 | - yes/no 17 | 18 | **Describe the bug** 19 | A clear and concise description of what the bug is. 20 | 21 | **To Reproduce** 22 | Steps to reproduce the behavior: 23 | 1. Go to '...' 24 | 2. Click on '....' 25 | 3. Scroll down to '....' 26 | 4. See error 27 | 28 | **Permalink to your prices** 29 | Please click the `Copy Permalink` button and paste it here 30 | 31 | **Expected behavior** 32 | A clear and concise description of what you expected to happen. 33 | 34 | **Screenshots** 35 | If applicable, add screenshots to help explain your problem. 36 | 37 | **Desktop (please complete the following information):** 38 | - OS: [e.g. iOS] 39 | - Browser [e.g. chrome, safari] 40 | - Version [e.g. 22] 41 | 42 | **Smartphone (please complete the following information):** 43 | - Device: [e.g. iPhone6] 44 | - OS: [e.g. iOS8.1] 45 | - Browser [e.g. stock browser, safari] 46 | - Version [e.g. 22] 47 | 48 | **Additional context** 49 | Add any other context about the problem here. 50 | -------------------------------------------------------------------------------- /js/themes.js: -------------------------------------------------------------------------------- 1 | function updateTheme(theme) { 2 | if (theme == "auto") { 3 | theme = (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) ? "dark" : "light"; 4 | } 5 | 6 | if (theme != "light") { 7 | document.documentElement.setAttribute("data-theme", theme); 8 | } else { 9 | document.documentElement.removeAttribute("data-theme"); 10 | } 11 | 12 | if (chart_instance && chart_options) { 13 | chart_instance.options = chart_options; 14 | chart_instance.update(); 15 | } 16 | } 17 | 18 | function setupTheming() { 19 | const themeSelector = $("#theme"); 20 | const supportsAutoTheming = (window.matchMedia && window.matchMedia("(prefers-color-scheme)").matches); 21 | let preferredTheme = localStorage.getItem("theme"); 22 | let selectorVal = preferredTheme ? preferredTheme : 23 | supportsAutoTheming ? "auto" : "light"; 24 | 25 | // Build theme option menu. 26 | if (supportsAutoTheming) { 27 | themeSelector.append(``); 28 | } 29 | themeSelector.append(``); 30 | themeSelector.append(``); 31 | 32 | themeSelector.val(selectorVal); 33 | 34 | // Listen to system changes in theme 35 | window.matchMedia("(prefers-color-scheme: dark)").addListener(() => { 36 | if (preferredTheme && preferredTheme != "auto") { return; } 37 | updateTheme("auto"); 38 | }); 39 | 40 | // Preference listener 41 | themeSelector.on('change', function () { 42 | preferredTheme = this.value; 43 | updateTheme(preferredTheme); 44 | 45 | if ((preferredTheme != "light" && !supportsAutoTheming) || 46 | (preferredTheme != "auto" && supportsAutoTheming)) { 47 | localStorage.setItem("theme", preferredTheme); 48 | } else { 49 | localStorage.removeItem("theme"); 50 | } 51 | }); 52 | } 53 | 54 | $(document).ready(function() { 55 | i18next.init((err, t) => { 56 | setupTheming(); 57 | }); 58 | }); -------------------------------------------------------------------------------- /js/translations.js: -------------------------------------------------------------------------------- 1 | function updateContent() { 2 | update(); 3 | $('body').localize(); 4 | } 5 | const defaultLanguage = 'en'; 6 | const LANGUAGES = { 7 | 'ca': 'Català', 8 | 'cs': 'Česky', 9 | 'de': 'Deutsch', 10 | 'en': 'English', 11 | 'es': 'Español', 12 | 'fr': 'Français', 13 | 'gl': 'Galego', 14 | 'hu': 'magyar', 15 | 'id': 'Bahasa Indonesia', 16 | 'it': 'Italiano', 17 | 'ja': '日本語', 18 | 'ko': '한국어', 19 | 'nl': 'Nederlands', 20 | 'ph': 'Filipino', 21 | 'pl': 'Polski', 22 | 'pt-BR': 'Português', 23 | 'ru': 'Русский', 24 | 'ua': 'Українська', 25 | 'th': 'ไทย', 26 | 'zh-CN': '简体中文', 27 | 'zh-TW': '繁體中文' 28 | }; 29 | i18next 30 | .use(i18nextXHRBackend) 31 | .use(i18nextBrowserLanguageDetector) 32 | .init({ 33 | fallbackLng: defaultLanguage, 34 | debug: true, 35 | backend: { 36 | loadPath: 'locales/{{lng}}.json', 37 | }, 38 | }, (err, t) => { 39 | languageSelector = $('#language'); 40 | for (let [code, name] of Object.entries(LANGUAGES)) { 41 | languageSelector.append(``); 42 | } 43 | for (let code of i18next.languages) { 44 | if (code in LANGUAGES) { 45 | languageSelector.val(code); 46 | $('html').attr('lang', code); 47 | break; 48 | } 49 | } 50 | languageSelector.on('change', function () { 51 | if (this.value == i18next.language) 52 | return; 53 | i18next.changeLanguage(this.value); 54 | $('html').attr('lang', this.value); 55 | }); 56 | jqueryI18next.init(i18next, $); 57 | i18next.on('languageChanged', lng => { 58 | updateContent(); 59 | }); 60 | // init set content 61 | $(document).ready(initialize); 62 | 63 | let delayTimer; 64 | $(document).on('input', function(event) { 65 | //prevent radio input from updating content twice per input change 66 | if(event.target.type === 'radio'){ return } 67 | // adding short delay after input to help mitigate potential lag after keystrokes 68 | clearTimeout(delayTimer); 69 | delayTimer = setTimeout(function() { 70 | updateContent(); 71 | }, 500); 72 | }); 73 | 74 | $('input[type = radio]').on('change', updateContent); 75 | }); 76 | -------------------------------------------------------------------------------- /service-worker.js: -------------------------------------------------------------------------------- 1 | // PWA Code adapted from https://github.com/pwa-builder/PWABuilder 2 | const CACHE = "pwa-precache-v1"; 3 | const precacheFiles = [ 4 | "/index.html", 5 | "/js/predictions.js", 6 | "/js/scripts.js", 7 | "/css/styles.css", 8 | "https://code.jquery.com/jquery-3.4.1.min.js", 9 | ]; 10 | 11 | self.addEventListener("install", function (event) { 12 | console.log("[PWA] Install Event processing"); 13 | 14 | console.log("[PWA] Skip waiting on install"); 15 | self.skipWaiting(); 16 | 17 | event.waitUntil( 18 | caches.open(CACHE).then(function (cache) { 19 | console.log("[PWA] Caching pages during install"); 20 | return cache.addAll(precacheFiles); 21 | }) 22 | ); 23 | }); 24 | 25 | // Allow sw to control of current page 26 | self.addEventListener("activate", function (event) { 27 | console.log("[PWA] Claiming clients for current page"); 28 | event.waitUntil(self.clients.claim()); 29 | }); 30 | 31 | // If any fetch fails, it will look for the request in the cache and serve it from there first 32 | self.addEventListener("fetch", function (event) { 33 | if (event.request.method !== "GET") return; 34 | 35 | event.respondWith( 36 | (async () => { 37 | let response; 38 | try { 39 | // Fetch from network first. 40 | response = await fetch(event.request); 41 | event.waitUntil(updateCache(event.request, response.clone())); 42 | } catch (error) { 43 | try { 44 | // Try if there's locally cached version. 45 | response = await fromCache(event.request); 46 | } catch (error) { 47 | console.log("[PWA] Network request failed and no cache." + error); 48 | throw error; 49 | } 50 | } 51 | return response; 52 | })() 53 | ); 54 | }); 55 | 56 | function fromCache(request) { 57 | // Check to see if you have it in the cache 58 | // Return response 59 | // If not in the cache, then return 60 | return caches.open(CACHE).then(function (cache) { 61 | return cache.match(request).then(function (matching) { 62 | if (!matching || matching.status === 404) { 63 | return Promise.reject("no-match"); 64 | } 65 | 66 | return matching; 67 | }); 68 | }); 69 | } 70 | 71 | function updateCache(request, response) { 72 | return caches.open(CACHE).then(function (cache) { 73 | return cache.put(request, response); 74 | }); 75 | } 76 | -------------------------------------------------------------------------------- /js/chart.js: -------------------------------------------------------------------------------- 1 | let chart_instance = null; 2 | 3 | Chart.defaults.global.defaultFontFamily = "'Varela Round', sans-serif"; 4 | 5 | const chart_options = { 6 | elements: { 7 | line: { 8 | get backgroundColor() { 9 | return getComputedStyle(document.documentElement).getPropertyValue('--chart-fill-color'); 10 | }, 11 | get borderColor() { 12 | return getComputedStyle(document.documentElement).getPropertyValue('--chart-line-color'); 13 | }, 14 | cubicInterpolationMode: "monotone", 15 | }, 16 | }, 17 | maintainAspectRatio: false, 18 | tooltips: { 19 | intersect: false, 20 | mode: "index", 21 | }, 22 | }; 23 | 24 | function update_chart(input_data, possibilities) { 25 | let ctx = $("#chart"), 26 | datasets = [{ 27 | label: i18next.t("output.chart.input"), 28 | get pointBorderColor() { 29 | return getComputedStyle(document.documentElement).getPropertyValue('--chart-point-color'); 30 | }, 31 | data: input_data.slice(1), 32 | fill: false, 33 | }, { 34 | label: i18next.t("output.chart.minimum"), 35 | get pointBorderColor() { 36 | return getComputedStyle(document.documentElement).getPropertyValue('--chart-point-color'); 37 | }, 38 | data: possibilities[0].prices.slice(1).map(day => day.min), 39 | fill: false, 40 | }, { 41 | label: i18next.t("output.chart.maximum"), 42 | get pointBorderColor() { 43 | return getComputedStyle(document.documentElement).getPropertyValue('--chart-point-color'); 44 | }, 45 | data: possibilities[0].prices.slice(1).map(day => day.max), 46 | fill: "-1", 47 | }, 48 | ], 49 | labels = [i18next.t("weekdays.sunday")].concat(...[i18next.t("weekdays.abr.monday"), i18next.t("weekdays.abr.tuesday"), i18next.t("weekdays.abr.wednesday"), i18next.t("weekdays.abr.thursday"), i18next.t("weekdays.abr.friday"), i18next.t("weekdays.abr.saturday")].map( 50 | day => [i18next.t("times.morning"), 51 | i18next.t("times.afternoon")].map( 52 | time => `${day} ${time}`))); 53 | 54 | if (chart_instance) { 55 | chart_instance.data.datasets = datasets; 56 | chart_instance.data.labels = labels; 57 | chart_instance.options = chart_options; 58 | chart_instance.update(); 59 | } else { 60 | chart_instance = new Chart(ctx, { 61 | data: { 62 | datasets: datasets, 63 | labels: labels 64 | }, 65 | options: chart_options, 66 | type: "line", 67 | }); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /locales/ko.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "daisy-mae": "무파니" 4 | }, 5 | "welcome": { 6 | "salutation": "안녕하세요, Nook Inc. 스마트폰의 Turnip Prophet앱을 사용해주셔서 감사합니다.", 7 | "description": "이 앱은 당신의 섬의 매일의 무 가격을 추적 할 수 있게 도와줍니다. 하지만 가격은 직접 입력해야 합니다!", 8 | "conclusion": "가격을 입력하고 나면, 이번 주 동안의 무 가격을 마법처럼 예측해줍니다." 9 | }, 10 | "first-time": { 11 | "title": "첫 구매", 12 | "description": "당신의 섬에 방문한 무파니에게서 처음으로 무를 샀습니까?(패턴에 영향을 끼칩니다)", 13 | "yes": "예", 14 | "no": "아니오" 15 | }, 16 | "patterns": { 17 | "title": "이전 패턴", 18 | "description": "저번 주의 무 가격 패턴이 어떻게 됩니까?(패턴에 영향을 끼칩니다)", 19 | "pattern": "패턴", 20 | "all": "모든 패턴", 21 | "decreasing": "감소", 22 | "fluctuating": "파동형", 23 | "unknown": "모름", 24 | "large-spike": "큰 급등", 25 | "small-spike": "작은 급등" 26 | }, 27 | "prices": { 28 | "description": "이번 주에 당신의 섬에서 무를 샀을 때의 가격이 어떻게 됩니까?", 29 | "open": { 30 | "am": "오전 - 오전 8:00 ~ 오전 11:59", 31 | "pm": "오후 - 오후 12:00 ~ 오후 10:00" 32 | }, 33 | "copy-permalink": "고유주소 복사", 34 | "permalink-copied": "고유주소가 복사되었습니다!", 35 | "reset": "Turnip Prophet 초기화", 36 | "reset-warning": "정말로 모든 입력한 값을 지우겠습니까?\n\n되돌릴 수 없습니다!" 37 | }, 38 | "weekdays": { 39 | "monday": "월요일", 40 | "tuesday": "화요일", 41 | "wednesday": "수요일", 42 | "thursday": "목요일", 43 | "friday": "금요일", 44 | "saturday" : "토요일", 45 | "sunday": "일요일", 46 | "abr": { 47 | "monday": "월", 48 | "tuesday": "화", 49 | "wednesday": "수", 50 | "thursday": "목", 51 | "friday": "금", 52 | "saturday" : "토" 53 | } 54 | }, 55 | "times": { 56 | "morning": "오전", 57 | "afternoon": "오후" 58 | }, 59 | "output": { 60 | "title": "결과", 61 | "chance": "% 확률", 62 | "to": "~", 63 | "minimum": "최저 보장 가격", 64 | "maximum": "가능한 최고 가격", 65 | "chart": { 66 | "input": "입력된 가격", 67 | "minimum": "최저 보장 가격", 68 | "maximum": "가능한 최고 가격" 69 | } 70 | }, 71 | "textbox": { 72 | "description": "지금까지의 무 가격을 입력하고 나면, Turnip Prophet이 당신의 섬에서 일어날 수 있는 가격 패턴을 보여줍니다.", 73 | "development": "이 앱은 아직 개발 중입니다. 앞으로도 계속해서 개선될 겁니다!", 74 | "thanks": "콩돌이와 밤돌이가 무 가격을 어떻게 결정하는지 알아낸 Ninji의 연구가 있었기에 이 앱이 만들어질 수 있었습니다.", 75 | "support": "GitHub을 통해서 문의, 의견 제시, 기여가 가능합니다", 76 | "contributors-text": "그리고 기여를 해주신 분들에게 감사를 표합니다!", 77 | "contributors": "기여해 주신 분들", 78 | "language": "언어" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /locales/zh-CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "daisy-mae": "曹卖" 4 | }, 5 | "welcome": { 6 | "salutation": "大家好,欢迎使用Nook手机上的大头菜预测工具。", 7 | "description": "这个APP可以让你每天跟踪自己岛上大头菜的价格,但你得自己把价格填写进去!", 8 | "conclusion": "之后,大头菜预测工具会神奇地预测出本周剩余时间的大头菜价格。" 9 | }, 10 | "first-time": { 11 | "title": "首次购买", 12 | "description": "你是第一次在自己岛上购买大头菜吗?(将影响预测趋势)", 13 | "yes": "是", 14 | "no": "否" 15 | }, 16 | "patterns": { 17 | "title": "上周趋势", 18 | "description": "上周大头菜的价格趋势是?(将影响预测趋势)", 19 | "pattern": "趋势", 20 | "all": "所有趋势", 21 | "decreasing": "递减型", 22 | "fluctuating": "波动型", 23 | "unknown": "不知道", 24 | "large-spike": "大幅上涨(三期型)", 25 | "small-spike": "小幅上涨(四期型)" 26 | }, 27 | "prices": { 28 | "description": "本周你的岛上大头菜的购买价格是多少?", 29 | "open": { 30 | "am": "上午 - 8:00 ~ 11:59", 31 | "pm": "下午 - 12:00 ~ 22:00" 32 | }, 33 | "copy-permalink": "复制价格分享链接", 34 | "permalink-copied": "链接已复制!", 35 | "reset": "重置大头菜预测工具", 36 | "reset-warning": "你确定要重置所有字段吗?\n\n此操作不可撤销!" 37 | }, 38 | "weekdays": { 39 | "monday": "周一", 40 | "tuesday": "周二", 41 | "wednesday": "周三", 42 | "thursday": "周四", 43 | "friday": "周五", 44 | "saturday" : "周六", 45 | "sunday": "周日", 46 | "abr": { 47 | "monday": "周一", 48 | "tuesday": "周二", 49 | "wednesday": "周三", 50 | "thursday": "周四", 51 | "friday": "周五", 52 | "saturday" : "周六" 53 | } 54 | }, 55 | "times": { 56 | "morning": "上午", 57 | "afternoon": "下午" 58 | }, 59 | "output": { 60 | "title": "结果", 61 | "chance": "几率(%)", 62 | "to": "~", 63 | "minimum": "保底价格", 64 | "maximum": "最高价格", 65 | "chart": { 66 | "input": "输入价格", 67 | "minimum": "保底价格", 68 | "maximum": "最高价格" 69 | } 70 | }, 71 | "textbox": { 72 | "description": "在填写一些大头菜价格后,大头菜预测工具将预测大头菜的价格并显示本周可能的趋势。", 73 | "development": "APP仍在开发中,但会随着时间的推移不断完善!", 74 | "thanks": "如果不是 Ninji 发现豆狸和粒狸如何给大头菜定价的,这一切将不可能实现。", 75 | "support": "可以在 GitHub 获得支持,或讨论和贡献", 76 | "sponsor": "想要赞助这个项目的开发者?进入 GitHub 并点击 ❤ Sponsor", 77 | "contributors-text": "哦!别忘记感谢那些至今为止做出过贡献的人。", 78 | "contributors": "贡献者", 79 | "language": "语言", 80 | "theme": { 81 | "title": "主题", 82 | "auto": "自动", 83 | "light": "亮色", 84 | "dark": "暗色" 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /locales/zh-TW.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "daisy-mae": "曹賣" 4 | }, 5 | "welcome": { 6 | "salutation": "你好,歡迎使用 Nook 手機上的 Turnip Prophet。", 7 | "description": "這個工具可以讓你每天追蹤自己島上的大頭菜價格,但你必須自己輸入價格!", 8 | "conclusion": "接下來,Turnip Prophet 將 神奇地 預測本週剩餘時間的大頭菜價格。" 9 | }, 10 | "first-time": { 11 | "title": "首次購買", 12 | "description": "這是你第一次從自己島上和曹賣購買大頭菜嗎?(將影響這次的模型)", 13 | "yes": "是", 14 | "no": "否" 15 | }, 16 | "patterns": { 17 | "title": "上次的模型", 18 | "description": "上週大頭菜的價格模型是什麼?(將影響這次的模型)", 19 | "pattern": "模型", 20 | "all": "所有模型", 21 | "decreasing": "遞減型", 22 | "fluctuating": "波型", 23 | "unknown": "不知道", 24 | "large-spike": "三期型", 25 | "small-spike": "四期型" 26 | }, 27 | "prices": { 28 | "description": "本週自己島上的大頭菜買價?", 29 | "open": { 30 | "am": "上午 - 08:00 到 11:59", 31 | "pm": "下午 - 12:00 到 22:00" 32 | }, 33 | "copy-permalink": "複製價格分享網址", 34 | "permalink-copied": "網址已複製!", 35 | "reset": "清除資料", 36 | "reset-warning": "是否確定要清除所有資料?\n\n此動作無法復原!" 37 | }, 38 | "weekdays": { 39 | "monday": "星期一", 40 | "tuesday": "星期二", 41 | "wednesday": "星期三", 42 | "thursday": "星期四", 43 | "friday": "星期五", 44 | "saturday" : "星期六", 45 | "sunday": "星期日", 46 | "abr": { 47 | "monday": "週一", 48 | "tuesday": "週二", 49 | "wednesday": "週三", 50 | "thursday": "週四", 51 | "friday": "週五", 52 | "saturday" : "週六" 53 | } 54 | }, 55 | "times": { 56 | "morning": "上午", 57 | "afternoon": "下午" 58 | }, 59 | "output": { 60 | "title": "結果", 61 | "chance": "機率(%)", 62 | "to": "~", 63 | "minimum": "保底價格", 64 | "maximum": "最高價格", 65 | "chart": { 66 | "input": "輸入價格", 67 | "minimum": "保底價格", 68 | "maximum": "最高價格" 69 | } 70 | }, 71 | "textbox": { 72 | "description": "在你記錄了一些大頭菜價格後,Turnip Prophet 會預測,並顯示自己島上可能出現的不同模型。", 73 | "development": "此工具仍在開發中,但會隨著時間的推移而改善!", 74 | "thanks": "要不是 Ninji 協助釐清豆狸和粒狸的大頭菜估價方式,這一切都不可能實現。", 75 | "support": "可於 GitHub 取得支援、討論及貢獻。", 76 | "sponsor": "想要贊助這個專案的開發者?進入 GitHub 並按下 ❤ Sponsor", 77 | "contributors-text": "嘿!別忘了感謝那些迄今為止作出貢獻的人!", 78 | "contributors": "貢獻者", 79 | "language": "語言", 80 | "theme": { 81 | "title": "主題", 82 | "auto": "自動", 83 | "light": "亮色", 84 | "dark": "暗色" 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /locales/ja.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "daisy-mae": "ウリ" 4 | }, 5 | "welcome": { 6 | "salutation": "こんにちは!タヌキ開発特製スマホの最新アプリ Turnip Prophet へようこそ。", 7 | "description": "このアプリは、無人島のカブ価を毎日予測することができます。そのためには、まず自分でデータを入力することが必要です!", 8 | "conclusion": "そうすれば、Turnip Prophetは魔法のように、あなたのこの一週間のカブ価を予測します。" 9 | }, 10 | "first-time": { 11 | "title": "はじめての購入", 12 | "description": "あなたが自分の島でカブを購入したのは今回が初めてですか?(答えによってパターンが変化します)", 13 | "yes": "はい", 14 | "no": "いいえ" 15 | }, 16 | "patterns": { 17 | "title": "先週のパターン", 18 | "description": "先週のカブ価変化パターンを選んでください。(答えによってパターンが変化します)", 19 | "pattern": "パターン", 20 | "all": "全てのパターン", 21 | "decreasing": "ジリ貧型", 22 | "fluctuating": "波型", 23 | "unknown": "わかりません", 24 | "large-spike": "跳ね大型(3期型)", 25 | "small-spike": "跳ね小型(4期型)" 26 | }, 27 | "prices": { 28 | "description": "今週のカブ価は?", 29 | "open": { 30 | "am": "午前 - AM 8:00 ~ AM 11:59", 31 | "pm": "午後 - PM 12:00 ~ PM 10:00" 32 | }, 33 | "copy-permalink": "パーマリンクをコピー", 34 | "permalink-copied": "パーマリンクがコピーされました!", 35 | "reset": "Turnip Prophetをリセット", 36 | "reset-warning": "本当に全てをリセットしますか?\n\nリセットしたら後戻りはできませんよ!" 37 | }, 38 | "weekdays": { 39 | "monday": "月曜日", 40 | "tuesday": "火曜日", 41 | "wednesday": "水曜日", 42 | "thursday": "木曜日", 43 | "friday": "金曜日", 44 | "saturday" : "土曜日", 45 | "sunday": "日曜日", 46 | "abr": { 47 | "monday": "月", 48 | "tuesday": "火", 49 | "wednesday": "水", 50 | "thursday": "木", 51 | "friday": "金", 52 | "saturday" : "土" 53 | } 54 | }, 55 | "times": { 56 | "morning": "午前", 57 | "afternoon": "午後" 58 | }, 59 | "output": { 60 | "title": "結果", 61 | "chance": "% 確率", 62 | "to": "~", 63 | "minimum": "保証される最小の収入", 64 | "maximum": "予測される限界の収入", 65 | "chart": { 66 | "input": "カブ価", 67 | "minimum": "保証される最小の収入", 68 | "maximum": "予測される限界の収入" 69 | } 70 | }, 71 | "textbox": { 72 | "description": "複数のカブ価を入力すると、Turnip Prophetはそれをもとに、可能性のある変動パターンを計算して表示します。", 73 | "development": "このアプリはまだ開発中ですが、より良いデータを提供できるようにがんばっています!", 74 | "thanks": "このアプリが作成できたのは、タヌキ商店のカブ価パターンを解析したNinji氏の成果のおかげです。ありがとうございます!", 75 | "support": "疑問·コメント·提案などは、GitHubまでお願いします。", 76 | "sponsor": "このプロジェクトに関わったソフトウェア開発者をスポンサーしたい場合、GitHubのページで「❤ Sponsor」をクリックしてください!", 77 | "contributors-text": "そして、このアプリの作成を手伝っていただいた方々に感謝します!", 78 | "contributors": "貢献者", 79 | "language": "言語", 80 | "theme": { 81 | "title": "テーマ", 82 | "auto": "自動", 83 | "light": "ライト", 84 | "dark": "ダーク" 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /locales/gl.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "daisy-mae": "Juliana" 4 | }, 5 | "welcome": { 6 | "salutation": "Ola, estás no aplicativo Turnip Prophet no teu Nookófono.", 7 | "description": "Este aplicativo permíteche facer un seguemento do prezo dos nabos na túa illa. Porén, tes que introducir os prezos manualmente.", 8 | "conclusion": "Depois, o aplicativo predirá maxicamente os prezos dos nabos na túa illa para o resto da semana." 9 | }, 10 | "first-time": { 11 | "title": "Primeira compra", 12 | "description": "É a primeira vez que compras nabos a Juliana na túa illa? (Isto afecta aos prezos)", 13 | "yes": "Si", 14 | "no": "Non" 15 | }, 16 | "patterns": { 17 | "title": "Tendencia anterior", 18 | "description": "Cal foi a tendencia de prezos na túa illa a semana pasada? (Isto afecta aos prezos)", 19 | "pattern": "Tendencia", 20 | "all": "Todas as tendencias", 21 | "decreasing": "Decrecente", 22 | "fluctuating": "Fluctuante", 23 | "unknown": "Descoñecido", 24 | "large-spike": "Pico grande", 25 | "small-spike": "Pico pequeno" 26 | }, 27 | "prices": { 28 | "description": "Que prezos tiveron os nabos esta semana na túa illa?", 29 | "open": { 30 | "am": "Mañá - 8:00 a 11:59", 31 | "pm": "Tarde - 12:00 a 22:00" 32 | }, 33 | "copy-permalink": "Copiar ligazón permanente", 34 | "permalink-copied": "Ligazón permanente copiada!", 35 | "reset": "Restablecer Turnip Prophet", 36 | "reset-warning": "Seguro que desexas restablecer todos os campos?\n\nIsto non se pode desfacer!" 37 | }, 38 | "weekdays": { 39 | "monday": "Luns", 40 | "tuesday": "Martes", 41 | "wednesday": "Mércores", 42 | "thursday": "Xoves", 43 | "friday": "Venres", 44 | "saturday" : "Sábado", 45 | "sunday": "Domingo", 46 | "abr": { 47 | "monday": "Lun", 48 | "tuesday": "Mar", 49 | "wednesday": "Mér", 50 | "thursday": "Xov", 51 | "friday": "Venr", 52 | "saturday" : "Sáb" 53 | } 54 | }, 55 | "times": { 56 | "morning": "Mañá", 57 | "afternoon": "Tarde" 58 | }, 59 | "output": { 60 | "title": "Resultado", 61 | "chance": "% Probabilidade", 62 | "to": "a", 63 | "minimum": "Mínimo garantido", 64 | "maximum": "Máximo potencial", 65 | "chart": { 66 | "input": "Prezo introducido", 67 | "minimum": "Mínimo garantido", 68 | "maximum": "Máximo potencial" 69 | } 70 | }, 71 | "textbox": { 72 | "description": "Despois de introducir algúns prezos, o Turnip Prophet calculará que prezos poden ter no futuro.", 73 | "development": "Este aplicativo aínda está en desenvolvemento, mais mellorará co tempo!", 74 | "thanks": "Nada disto sería posible sen o traballo de Ninji achando como se calculan os prezos.", 75 | "support": "Soporte, comentarios e contribucións están dispoñibles en GitHub", 76 | "contributors-text": "Oh! E non esqueceremos dar as grazas aos que xa contribuíron.", 77 | "contributors": "Contribuidores", 78 | "language": "Lingua" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /locales/th.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "daisy-mae": "Daisy Mae" 4 | }, 5 | "welcome": { 6 | "salutation": "สวัสดีจ้า ยินดีต้อนรับสู่โปรแกรม Turnip Prophet บน Nook Phone ของคุณ", 7 | "description": "โปรแกรมนี้จะช่วยให้คุณสามารถจดบันทึกราคาหัวผักกาดรายวันบนเกาะของคุณ แต่ต้องจดเองนะ!", 8 | "conclusion": "หลังจากนั้น Turnip Prophet จะทำการพยากรณ์ราคาหัวผักกาดตลอดทั้งสัปดาห์ที่เป็นไปได้ให้คุณ" 9 | }, 10 | "first-time": { 11 | "title": "ซื้อครั้งแรก", 12 | "description": "นี่เป็นครั้งแรกที่คุณซื้อหัวผักกาดจากน้องหมูอู๊ดๆ Daisy Mae บนเกาะของคุณรีเปล่า?(มีผลกับการคำนวณรูปแบบราคา)", 13 | "yes": "ใช่", 14 | "no": "ไม่" 15 | }, 16 | "patterns": { 17 | "title": "รูปแบบก่อนหน้า", 18 | "description": "รูปแบบของราคาสัปดาห์ที่แล้ว?(มีผลกับการคำนวณรูปแบบราคา)", 19 | "pattern": "รูปแบบ", 20 | "all": "รูปแบบทั้งหมด", 21 | "decreasing": "ลดลง", 22 | "fluctuating": "ผันผวน", 23 | "unknown": "ไม่รู้สิ!", 24 | "large-spike": "พุ่งขึ้นสูงมาก", 25 | "small-spike": "พุ่งขึ้นเล็กน้อย" 26 | }, 27 | "prices": { 28 | "description": "ราคาหัวผักกาดบนเกาะของคุณสัปดาห์นี้?", 29 | "open": { 30 | "am": "เช้า - 08.00 นาฬิกา ถึง 11.59 นาฬิกา", 31 | "pm": "บ่าย - 12:00 นาฬิกา to 22:00 นาฬิกา" 32 | }, 33 | "copy-permalink": "คัดลอก Permalink", 34 | "permalink-copied": "Permalink คัดลอกแล้วจ้า!", 35 | "reset": "รีเซ็ต Turnip Prophet", 36 | "reset-warning": "แน่ใจนะว่าจะล้างข้อมูลทั้งหมด?\n\nแก้ไขอีกไม่ได้แล้วนา!" 37 | }, 38 | "weekdays": { 39 | "monday": "วันจันทร์", 40 | "tuesday": "วันอังคาร", 41 | "wednesday": "วันพุธ", 42 | "thursday": "วันพฤหัสบดี", 43 | "friday": "วันศุกร์", 44 | "saturday" : "วันเสาร์", 45 | "sunday": "วันอาทิตย์", 46 | "abr": { 47 | "monday": "จันทร์", 48 | "tuesday": "อังคาร", 49 | "wednesday": "พุธ", 50 | "thursday": "พฤหัส", 51 | "friday": "ศุกร์", 52 | "saturday" : "เสาร์" 53 | } 54 | }, 55 | "times": { 56 | "morning": "เช้า", 57 | "afternoon": "บ่าย" 58 | }, 59 | "output": { 60 | "title": "ผลลัพธ์", 61 | "chance": "โอกาส %", 62 | "to": "ถึง", 63 | "minimum": "การันตีต่ำสุด", 64 | "maximum": "เป็นไปได้สูงสุด", 65 | "chart": { 66 | "input": "ราคา", 67 | "minimum": "การันตีต่ำสุด", 68 | "maximum": "เป็นไปได้สูงสุด" 69 | } 70 | }, 71 | "textbox": { 72 | "description": "หลังจากใส่ราคาหัวผักกาดไปได้บางส่วน โปรแกรม Turnip Prophet จะทำการแสดงตัวเลขราคาและความเป็นไปได้ของรูปแบบต่าง ๆ ที่เกาะของคุณจะเจอ", 73 | "development": "โปรแกรมนี้ยังอยู่ในขั้นตอนพัฒนา แต่จะค่อยๆปรับปรุงขึ้นเรื่อยๆ!", 74 | "thanks": "ทั้งหมดนี้ไม่อาจเกิดขึ้นได้เลยถ้าปราศจากผลงานของ Ninji ที่ค้นพบวิธีการให้ราคาหัวผักกาดของทานุกิน้อย Timmy และ Tommy", 75 | "support": "สนับสนุน, แสดงความเห็นและช่วยพัฒนาได้ที่ GitHub", 76 | "sponsor": "ต้องการเป็นสปอนเซอร์แก่นักพัฒนา? ไปที่ GitHub และเลือก '❤ Sponsor'", 77 | "contributors-text": "อ้อ! และต้องไม่ลืมที่จะขอบคุณผู้ที่ช่วยร่วมพัฒนามาจนถึงตอนนี้!", 78 | "contributors": "ผู้ร่วมพัฒนา", 79 | "language": "ภาษา" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /locales/nl.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "daisy-mae": "Daisy Mae" 4 | }, 5 | "welcome": { 6 | "salutation": "Hallo, en welkom bij de Turnip Prophet app op je Nook Phone.", 7 | "description": "Deze app laat je toe de knol-prijzen op je eiland dagelijks bij te houden, maar je moet ze zelf ingeven!", 8 | "conclusion": "Daarna zal de Turnip Prophet app op magische wijze de knol-prijzen berekenen die je de rest van de week zal krijgen." 9 | }, 10 | "first-time": { 11 | "title": "Beginnende Koper", 12 | "description": "Is dit de eerste keer dat je knollen koopt van Daisy Mae op je eiland?(Dit beïnvloedt je patroon)", 13 | "yes": "Ja", 14 | "no": "Nee" 15 | }, 16 | "patterns": { 17 | "title": "Vorige Patroon", 18 | "description": "Wat was het patroon van je knol-prijzen vorige week?(Dit beïnvloedt je patroon)", 19 | "pattern": "Patroon", 20 | "all": "Alle patronen", 21 | "decreasing": "Dalend", 22 | "fluctuating": "Schommelend", 23 | "unknown": "Ik weet het niet", 24 | "large-spike": "Grote Piek", 25 | "small-spike": "Kleine Piek" 26 | }, 27 | "prices": { 28 | "description": "Wat was de prijs van knollen op je eiland deze week?", 29 | "open": { 30 | "am": "Voormiddag - 8:00 tot 11:59", 31 | "pm": "Namiddag - 12:00 tot 22:00" 32 | }, 33 | "copy-permalink": "Kopieer deelbare link", 34 | "permalink-copied": "Deelbare link gekopieerd!", 35 | "reset": "Maak Turnip Prophet leeg", 36 | "reset-warning": "Ben je zeker dat je alle velden wil leegmaken?\n\nDit kan niet ongedaan gemaakt worden!" 37 | }, 38 | "weekdays": { 39 | "monday": "Maandag", 40 | "tuesday": "Dinsdag", 41 | "wednesday": "Woensdag", 42 | "thursday": "Donderdag", 43 | "friday": "Vrijdag", 44 | "saturday" : "Zaterdag", 45 | "sunday": "Zondag", 46 | "abr": { 47 | "monday": "Ma", 48 | "tuesday": "Di", 49 | "wednesday": "Woe", 50 | "thursday": "Do", 51 | "friday": "Vr", 52 | "saturday" : "Za" 53 | } 54 | }, 55 | "times": { 56 | "morning": "VM", 57 | "afternoon": "NM" 58 | }, 59 | "output": { 60 | "title": "Resultaat", 61 | "chance": "% Kans", 62 | "to": "tot", 63 | "minimum": "Gegarandeerd Minimum", 64 | "maximum": "Potentieel Maximum", 65 | "chart": { 66 | "input": "Ingevoerde prijs", 67 | "minimum": "Gegarandeerd Minimum", 68 | "maximum": "Potentieel Maximum" 69 | } 70 | }, 71 | "textbox": { 72 | "description": "Nadat je wat knol-prijzen hebt ingevoerd, zal de Turnip Prophet wat berekeningen doen en de verschillende mogelijke patronen voorstellen die je eiland kan vertonen.", 73 | "development": "Deze applicatie is nog in ontwikkeling, en zal met de tijd beter worden!", 74 | "thanks": "Dit alles zou niet mogelijk zijn zonder het werk van Ninji om uit te zoeken hoe Timmy en Tommy de waarde van knollen bepalen.", 75 | "support": "Ondersteuning, feedback en bijdrages zijn mogelijk via GitHub", 76 | "sponsor": "Wil je de ontwikkelaars van dit project sponsoren? Ga naar de GitHub pagina en klik op '❤ Sponsor'", 77 | "contributors-text": "Oh! En laten we degenen die tot nu toe bijgedragen hebben niet vergeten te bedanken!", 78 | "contributors": "Bijdragers", 79 | "language": "Taal" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /locales/pl.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "daisy-mae": "Daisy Mae" 4 | }, 5 | "welcome": { 6 | "salutation": "Cześć, witaj w aplikacji Turnip Prophet na Twoim Nook Phone.", 7 | "description": "Ta aplikacja pozwoli Ci codziennie śledzić ceny rzep na twojej wyspie, jednak będziesz musiał uzupełniać je samodzielnie.", 8 | "conclusion": "Następnie aplikacja Turnip Prophet w magiczny sposób przewidzi jakie ceny rzep będziesz mieć przez resztę tygodnia" 9 | }, 10 | "first-time": { 11 | "title": "Kupujący po raz pierwszy", 12 | "description": "Czy jest to pierwszy raz gdy kupujesz rzepy od Daisy Mae na własnej wyspie?(Ta opcja wpłynie na Twój schemat)", 13 | "yes": "Tak", 14 | "no": "Nie" 15 | }, 16 | "patterns": { 17 | "title": "Poprzedni schemat", 18 | "description": "Jaka była tendencja cen rzep w poprzednim tygodniu?(Ta opcja wpłynie na Twój wzór)", 19 | "pattern": "Wzór", 20 | "all": "Wszystkie wzory", 21 | "decreasing": "Malejąca", 22 | "fluctuating": "Zmienna", 23 | "unknown": "Nie pamiętam", 24 | "large-spike": "Duży wzrost", 25 | "small-spike": "Mały wzrost" 26 | }, 27 | "prices": { 28 | "description": "Jakie ceny za rzepy były w tym tygodniu na Twojej wyspie?", 29 | "open": { 30 | "am": "Przed południem - 8:00 do 11:59 ", 31 | "pm": "Po południu - 12:00 do 22:00 " 32 | }, 33 | "copy-permalink": "Skopiuj permalink", 34 | "permalink-copied": "Permalink skopiowany!", 35 | "reset": "Wyzeruj Turnip Prophet", 36 | "reset-warning": "Czy jesteś pewien, że chcesz wyzerować wszystkie pola?\n\nTej opcji nie można cofnąć!" 37 | }, 38 | "weekdays": { 39 | "monday": "Poniedziałek", 40 | "tuesday": "Wtorek", 41 | "wednesday": "Środa", 42 | "thursday": "Czwartek", 43 | "friday": "Piątek", 44 | "saturday" : "Sobota", 45 | "sunday": "Niedziela", 46 | "abr": { 47 | "monday": "Pon", 48 | "tuesday": "Wt", 49 | "wednesday": "Śr", 50 | "thursday": "Czw", 51 | "friday": "Pt", 52 | "saturday" : "Sob" 53 | } 54 | }, 55 | "times": { 56 | "morning": "Rano", 57 | "afternoon": "Popołudnie" 58 | }, 59 | "output": { 60 | "title": "Wyniki", 61 | "chance": "Szansa %", 62 | "to": "do", 63 | "minimum": "Gwarantowana cena minimalna", 64 | "maximum": "Potencjalna najwyższa cena", 65 | "chart": { 66 | "input": "Cena wejściowa", 67 | "minimum": "Gwarantowana cena minimalna", 68 | "maximum": "Potencjalna najwyższa cena" 69 | } 70 | }, 71 | "textbox": { 72 | "description": "Po tym jak wprowadzisz kilka cen rzep, Turnip Prophet dokona obliczeń i wyświetli różne prawdopodobne szablony cen, których może doświadczyć Twoja wyspa", 73 | "development": "Ta aplikacja wciąż jest w produkcji, lecz z czasem zostanie poprawiona!", 74 | "thanks": "Nie udałoby nam się to bez pracy Ninji, który dowiedział się jak Timmy and Tommy wyceniają rzepy.", 75 | "support": "Wsparcie, komentarze i wpłaty są możliwe pod adresem GitHub", 76 | "sponsor": "Chcesz wesprzeć twórców tego projektu? Wejdź na stronę GitHub i wciśnij '❤ Sponsor'", 77 | "contributors-text": "Aha! I nie zapominajmy o tych, którzy dotychczas nas wspierali!", 78 | "contributors": "Współautorzy", 79 | "language": "Język" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /locales/cs.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "daisy-mae": "Daisy Mae" 4 | }, 5 | "welcome": { 6 | "salutation": "Ahoj, vítej v aplikaci Turnip Prophet tvého Nook Phone.", 7 | "description": "Tahle aplikace by ti měla pomoct sledovat vývoj cen tuřínů na tvém ostrově. Musíš si ale sám zadávat ceny, které už znáš!", 8 | "conclusion": "Po zadání Turnip Prophet kouzelným způsobem předpoví, jak by se ceny měly vyvíjet v průběhu týdne." 9 | }, 10 | "first-time": { 11 | "title": "Poprvé kupující", 12 | "description": "Je to poprvé, co kupuješ tuříny od Daisy na vlastním ostrově?(Ovlivňuje model vývoje cen)", 13 | "yes": "Ano", 14 | "no": "Ne" 15 | }, 16 | "patterns": { 17 | "title": "Předchozí model", 18 | "description": "Jaký byl model vývoje cen tuřínů na tvém ostrově minulý týden?(Ovlivňuje model vývoje cen)", 19 | "pattern": "Model", 20 | "all": "Všechny modely", 21 | "decreasing": "Klesající", 22 | "fluctuating": "Kolísavý", 23 | "unknown": "Nevím", 24 | "large-spike": "S velkou špicí", 25 | "small-spike": "S malou špicí" 26 | }, 27 | "prices": { 28 | "description": "Jaké byly ceny tuřínů v průběhu tohoto týdne?", 29 | "open": { 30 | "am": "dop. - od 8:00 do 11:59", 31 | "pm": "odp. - od 12:00 do 22:00" 32 | }, 33 | "copy-permalink": "Kopírovat permalink", 34 | "permalink-copied": "Permalink zkopírován!", 35 | "reset": "Resetovat Turnip Prophet", 36 | "reset-warning": "Opravdu chceš resetovat všechny pole?\n\nNejde to vrátit zpět!" 37 | }, 38 | "weekdays": { 39 | "monday": "Pondělí", 40 | "tuesday": "Úterý", 41 | "wednesday": "Středa", 42 | "thursday": "Čtvrtek", 43 | "friday": "Pátek", 44 | "saturday" : "Sobota", 45 | "sunday": "Neděle", 46 | "abr": { 47 | "monday": "Po", 48 | "tuesday": "Út", 49 | "wednesday": "St", 50 | "thursday": "Čt", 51 | "friday": "Pá", 52 | "saturday" : "So" 53 | } 54 | }, 55 | "times": { 56 | "morning": "dop.", 57 | "afternoon": "odp." 58 | }, 59 | "output": { 60 | "title": "Výstup", 61 | "chance": "Šance %", 62 | "to": "až", 63 | "minimum": "Jisté Minimum", 64 | "maximum": "Možné Maximum", 65 | "chart": { 66 | "input": "Vstupní cena", 67 | "minimum": "Jisté Minimum", 68 | "maximum": "Možné Maximum" 69 | } 70 | }, 71 | "textbox": { 72 | "description": "Po zadání několika cen tuřínů, Turnip Prophet provede nějaké výpočty a zobrazí všechny možné modely vývoje cen, které mohou na tvém ostrově nastat.", 73 | "development": "Aplikace je pořád ve vývoji, časem se stále zlepšuje!", 74 | "thanks": "Nic z toho by nebylo možné bez Ninjiho výzkumu, jak vlastně Timmy a Tommy hodnotí cenu tuřínů.", 75 | "support": "Podpora, komentáře a příspěvky jsou možné přes GitHub", 76 | "sponsor": "Chceš podpořit vývojáře stojící za tímto projektem? Jdi na GitHub a klikni na tlačítko '❤ Sponsor'", 77 | "contributors-text": "Jo! A nesmíme zapomínat poděkovat těm, kteří se na vývoji tohoto projektu zatím podíleli!", 78 | "contributors": "Na projektu se podílí", 79 | "language": "Jazyk", 80 | "theme": { 81 | "title": "Barevné schéma", 82 | "auto": "Automaticky", 83 | "light": "Světlé", 84 | "dark": "Tmavé" 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Animal Crossing New Horizons: Turnip Prophet 2 | [![discord](https://img.shields.io/badge/discord-join-7289DA.svg?logo=discord&longCache=true&style=for-the-badge)](https://discord.gg/bRh74X8) 3 | [![issues](https://img.shields.io/github/issues/mikebryant/ac-nh-turnip-prices?style=for-the-badge)](https://github.com/mikebryant/ac-nh-turnip-prices/issues) 4 | [![pull requests](https://img.shields.io/github/issues-pr/mikebryant/ac-nh-turnip-prices?style=for-the-badge)](https://github.com/mikebryant/ac-nh-turnip-prices/pulls) 5 | [![contributors](https://img.shields.io/github/contributors/mikebryant/ac-nh-turnip-prices?style=for-the-badge)](https://github.com/mikebryant/ac-nh-turnip-prices/graphs/contributors) 6 | 7 | Turnip Prophet is a price calculator/price predictor for Animal Crossing: New Horizons turnip prices. 8 | 9 | ## Support 10 | 11 | If you have any questions, feel free to join our [Discord server](https://discord.gg/bRh74X8) to ask or [open a new issue](https://github.com/mikebryant/ac-nh-turnip-prices/issues). 12 | 13 | If you have a prediction issue, please open an issue describing your problem and give the permalink to your prediction. Otherwise, please search the issues before opening a new one to make sure you are not opening a duplicate issue. 14 | 15 | Please create issues in English language only. 16 | 17 | ## What about feature X? 18 | 19 | At first please have a look at our current project scope: 20 | 21 | | Turnip Prophet is | Turnip Prophet is not | 22 | |----|----| 23 | | A predictor for future prices that week | A calculator for how much money you'll make | 24 | | Able to calculate probabilities for different futures | A way to count your turnips | 25 | | Able to show data from a query string | A way to store multiple people's islands | 26 | | A single page web-based app | Something with a backend | 27 | 28 | If your idea, suggestion or improvement is anything out of the above named, feel free to [open a new issue](https://github.com/mikebryant/ac-nh-turnip-prices/issues) or contribute by a [new pull request](https://github.com/mikebryant/ac-nh-turnip-prices/pulls). 29 | 30 | ## How to run the project locally? 31 | 32 | To run the project locally you will have to clone it and then, from the folder you just cloned, you will have to execute a command. There are multiple options, listed below: 33 | 34 | ### Using Python 35 | 36 | For Python 2.7: 37 | 38 | ```python -m SimpleHTTPServer``` 39 | 40 | For Python 3: 41 | 42 | ```python3 -m http.server``` 43 | 44 | ### Using Node.js 45 | 46 | ```npx serve``` 47 | 48 | ### Using Chrome 49 | 50 | ```google-chrome --allow-file-access-from-files``` 51 | 52 | 53 | ## Adding a new language 54 | 55 | Turnip Prophet is already available in some languages. If your local language is not listed you may go on to create a JSON file corresponding to your language in the folder [locales](https://github.com/mikebryant/ac-nh-turnip-prices/tree/master/locales). You may copy the [English localisation](https://github.com/mikebryant/ac-nh-turnip-prices/blob/master/locales/en.json) and translate it. 56 | 57 | Please make sure **not to translate** "Turnip Prophet" and include the new language in the selector inside [js/translations.js](https://github.com/mikebryant/ac-nh-turnip-prices/blob/master/js/translations.js). 58 | 59 | If you have any remaining questions, feel free to stop by the Discord server and ask. 60 | 61 | 62 | ## Final statement 63 | 64 | A special thanks to all who [contribute](https://github.com/mikebryant/ac-nh-turnip-prices/graphs/contributors) to this project, helping to improve it and spend their time. 65 | 66 | Stay awesome guys. 67 | -------------------------------------------------------------------------------- /locales/it.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "name": "Turnip Prophet", 4 | "daisy-mae": "Brunella" 5 | }, 6 | "welcome": { 7 | "salutation": "Ciao e benvenuto nell'app Turnip Prophet del tuo Nook Phone.", 8 | "description": "Questa applicazione ti permetterà di tenere traccia del prezzo giornaliero delle rape. Ma dovrai inserire i prezzi da te!", 9 | "conclusion": "Se lo fai Turnip Prophet predirrà magicamente i prezzi delle rape che avrai per il resto della settimana." 10 | }, 11 | "first-time": { 12 | "title": "Primo Acquisto", 13 | "description": "È la prima volta che acquisti sulla tua isola le rape da Brunella?(influisce sul comportamento dei prezzi)", 14 | "yes": "Sì", 15 | "no": "No" 16 | }, 17 | "patterns": { 18 | "title": "Comportamento Precedente", 19 | "description": "Qual è stato il comportamento dei prezzi delle rape nella scorsa settimana?(influisce sul comportamento dei prezzi)", 20 | "pattern": "Comportamento", 21 | "all": "Tutti i comportamenti", 22 | "decreasing": "Decrescente", 23 | "fluctuating": "Oscillante", 24 | "unknown": "Non lo so!", 25 | "large-spike": "Grande picco", 26 | "small-spike": "Piccolo picco" 27 | }, 28 | "prices": { 29 | "description": "Qual era il prezzo di acquisto delle rape sulla tua isola questa settimana?", 30 | "open": { 31 | "am": "Mattina - dalle 8:00 alle 11:59", 32 | "pm": "Pomeriggio - dalle 12:00 alle 22:00" 33 | }, 34 | "copy-permalink": "Copia permalink", 35 | "permalink-copied": "Permalink copiato!", 36 | "reset": "Resetta Turnip Prophet", 37 | "reset-warning": "Sei sicuro di voler resettare tutti i campi?\n\nNon può essere annullato!" 38 | }, 39 | "weekdays": { 40 | "monday": "Lunedì", 41 | "tuesday": "Martedì", 42 | "wednesday": "Mercoledì", 43 | "thursday": "Giovedì", 44 | "friday": "Venerdì", 45 | "saturday": "Sabato", 46 | "sunday": "Domenica", 47 | "abr": { 48 | "monday": "Lun", 49 | "tuesday": "Mar", 50 | "wednesday": "Mer", 51 | "thursday": "Gio", 52 | "friday": "Ven", 53 | "saturday": "Sab" 54 | } 55 | }, 56 | "times": { 57 | "morning": "AM", 58 | "afternoon": "PM" 59 | }, 60 | "output": { 61 | "title": "Risultati", 62 | "chance": "Probabilità %", 63 | "to": "a", 64 | "minimum": "Minimo Garantito", 65 | "maximum": "Massimo Potenziale", 66 | "chart": { 67 | "input": "Prezzo Iniziale", 68 | "minimum": "Minimo Garantito", 69 | "maximum": "Massimo Potenziale" 70 | } 71 | }, 72 | "textbox": { 73 | "description": "Dopo aver inserito alcuni prezzi, Turnip Prophet calcolerà e mostrerà i possibili comportamenti del prezzo delle rape nella tua isola.", 74 | "development": "Quest'applicazione è in ancora in sviluppo ma migliorerà col tempo!", 75 | "thanks": "Niente di questo sarebbe possibile senza il lavoro di Ninji nello scoprire come Mirco e Marco valutano le rape.", 76 | "support": "Chiedi supporto o lascia commenti e contributi su GitHub", 77 | "sponsor": "Vuoi sponsorizzare gli sviluppatori di questo progetto? Vai nella pagina GitHub e clicca '❤ Sponsor'", 78 | "contributors-text": "Oh! Non dimentichiamoci di ringraziare chi ha contribuito fin'ora!", 79 | "contributors": "Collaboratori", 80 | "language": "Lingua" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /locales/ca.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "daisy-mae": "Juliana" 4 | }, 5 | "welcome": { 6 | "salutation": "Hola! Et donem la benvinguda a l'app Turnip Prophet pel teu Nookòfon.", 7 | "description": "Aquesta app et permet predir els preus dels naps a la teva illa, però hauràs d'introduir-hi tu els preus passats!", 8 | "conclusion": "Fet això, l'app Turnip Prophet endevinarà els preus que tindràs durant la resta de la setmana." 9 | }, 10 | "first-time": { 11 | "title": "Primer cop que compres", 12 | "description": "És la primera vegada que li compres naps a la Juliana a la teva illa?(Afecta al patró de preus)", 13 | "yes": "Sí", 14 | "no": "No" 15 | }, 16 | "patterns": { 17 | "title": "Patró anterior", 18 | "description": "Quin patró tenien els teus preus la setmana passada?(Afecta al patró actual)", 19 | "pattern": "Patró", 20 | "all": "Tots els patrons", 21 | "decreasing": "Decreixent", 22 | "fluctuating": "Fluctuant", 23 | "unknown": "No ho sé", 24 | "large-spike": "Pic gran", 25 | "small-spike": "Pic petit" 26 | }, 27 | "prices": { 28 | "description": "Quin ha sigut el preu de compra de naps a la teva illa aquesta setmana?", 29 | "open": { 30 | "am": "AM - De les 8:00 am a les 11:59 am", 31 | "pm": "PM - De les 12:00 pm a les 10:00 pm" 32 | }, 33 | "copy-permalink": "Copiar permalink", 34 | "permalink-copied": "Permalink copiat!", 35 | "reset": "Reiniciar Turnip Prophet", 36 | "reset-warning": "Segur que vols reiniciar tots els camps?\n\nNo pots desfer aquesta acció!" 37 | }, 38 | "weekdays": { 39 | "monday": "Dilluns", 40 | "tuesday": "Dimarts", 41 | "wednesday": "Dimecres", 42 | "thursday": "Dijous", 43 | "friday": "Divendres", 44 | "saturday" : "Dissabte", 45 | "sunday": "Diumenge", 46 | "abr": { 47 | "monday": "Dl", 48 | "tuesday": "Dm", 49 | "wednesday": "Dc", 50 | "thursday": "Dj", 51 | "friday": "Dv", 52 | "saturday" : "Ds" 53 | } 54 | }, 55 | "times": { 56 | "morning": "AM", 57 | "afternoon": "PM" 58 | }, 59 | "output": { 60 | "title": "Resultat", 61 | "chance": "Probabilitat (%)", 62 | "to": "a", 63 | "minimum": "Mínim garantit", 64 | "maximum": "Màxim potencial", 65 | "chart": { 66 | "input": "Preu d'entrada", 67 | "minimum": "Mínim garantit", 68 | "maximum": "Màxim potencial" 69 | } 70 | }, 71 | "textbox": { 72 | "description": "Quan hagis posat els preus dels naps, el Turnip Prophet farà comptes i et mostrarà els possibles patrons que puguin haver a la teva illa.", 73 | "development": "L'app encara està en desenvolupament, però l'anirem millorant!", 74 | "thanks": "Aquest projecte ha estat possible gràcies a en Ninji, que va descobrir com calculen en Tendo i en Nendo els preus dels naps.", 75 | "support": "A GitHub hi trobaràs suport, comentaris, i contribucions.", 76 | "sponsor": "Vols patrocinar als desenvolupadors del projecte? Ves a GitHub i clica damunt de '❤ Sponsor'", 77 | "contributors-text": "Ah! I no ens oblidem dels que ja han ajudat amb les seves contribucions!", 78 | "contributors": "Contribuidors", 79 | "language": "Llenguatge", 80 | "theme": { 81 | "title": "Tema", 82 | "auto": "Automàtic", 83 | "light": "Clar", 84 | "dark": "Fosc" 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /locales/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "daisy-mae": "Дейзи Мэй" 4 | }, 5 | "welcome": { 6 | "salutation": "Добрый день! Добро пожаловать в приложение Предсказатель на Вашем Нукофоне.", 7 | "description": "Это приложение позволяет Вам ежедневно отслеживать стоимость репы на Вашем острове. Будьте внимательны, Вам придется вводить ее вручную!", 8 | "conclusion": "После этого Предсказатель как по волшебству предскажет стоимость репы в Вашем магазине на этой неделе." 9 | }, 10 | "first-time": { 11 | "title": "Новичок рынка репы", 12 | "description": "Вы впервые приобретаете репу у Дейзи Мэй на своем острове?(Это влияет на модель стоимости)", 13 | "yes": "Да", 14 | "no": "Нет" 15 | }, 16 | "patterns": { 17 | "title": "Предыдущая модель", 18 | "description": "Какая модель стоимости репы была у Вас на прошлой неделе?(Это влияет на модель стоимости)", 19 | "pattern": "Модель", 20 | "all": "Все модели", 21 | "decreasing": "Постоянное снижение", 22 | "fluctuating": "Колебание стоимости", 23 | "unknown": "Не могу сказать точно", 24 | "large-spike": "Большой скачок", 25 | "small-spike": "Малый скачок" 26 | }, 27 | "prices": { 28 | "description": "Какова была стоимость репы у Дейзи Мэй на Вашем острове на этой неделе?", 29 | "open": { 30 | "am": "Утро - 8:00 - 11:59", 31 | "pm": "День - 12:00 - 22:00" 32 | }, 33 | "copy-permalink": "Скопировать постоянную ссылку", 34 | "permalink-copied": "Постоянная ссылка скопирована!", 35 | "reset": "Перезагрузить Предсказателя", 36 | "reset-warning": "Вы уверены, что хотите обнулить все поля?\n\nДанное действие необратимо!" 37 | }, 38 | "weekdays": { 39 | "monday": "Понедельник", 40 | "tuesday": "Вторник", 41 | "wednesday": "Среда", 42 | "thursday": "Четверг", 43 | "friday": "Пятница", 44 | "saturday" : "Суббота", 45 | "sunday": "Воскресенье", 46 | "abr": { 47 | "monday": "Пн", 48 | "tuesday": "Вт", 49 | "wednesday": "Ср", 50 | "thursday": "Чт", 51 | "friday": "Пт", 52 | "saturday" : "Сб" 53 | } 54 | }, 55 | "times": { 56 | "morning": "Утро", 57 | "afternoon": "День" 58 | }, 59 | "output": { 60 | "title": "Вывод модели", 61 | "chance": "Вероятность в %", 62 | "to": "-", 63 | "minimum": "Гарантированный минимум", 64 | "maximum": "Возможный максимум", 65 | "chart": { 66 | "input": "Введенная стоимость", 67 | "minimum": "Гарантированный минимум", 68 | "maximum": "Возможный максимум" 69 | } 70 | }, 71 | "textbox": { 72 | "description": "После того, как Вы введете уже известную Вам стоимость репы, Препсказатель посчитает и покажет различные вероятные для Вашего острова модели стоимости репы.", 73 | "development": "Это приложение пока еще в разработке, но со временем обязательно станет лучше!", 74 | "thanks": "Если бы Ninji не смог разузнать, как Тимми и Томми определяют стоимость репы, данного приложения могло бы и не быть!", 75 | "support": "Поддержать нас, прокомментировать, а также внести свой вклад Вы можете на GitHub", 76 | "sponsor": "Хотели бы поддержать разработчиков проекта? Для этого Вы можете перейти на GitHub и нажать '❤ Sponsor'", 77 | "contributors-text": "Точно! Нельзя забывать тех, кто уже помог проекту!", 78 | "contributors": "Вклад внесли", 79 | "language": "Язык" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /locales/ua.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "daisy-mae": "Дейзі Мей" 4 | }, 5 | "welcome": { 6 | "salutation": "Добрий день! Вітаємо в додатку Turnip Prophet на Вашому Нукофоні.", 7 | "description": "Цей додаток дозволяє Вам відслідковувати ціни ріпи на Вашому острові щоденно, але Вам доведеться вписати ціни власноруч!", 8 | "conclusion": "Після цього, додаток Turnip Prophet магічним способом передбачить ціни ріпи на Вашому острові на решту тижня." 9 | }, 10 | "first-time": { 11 | "title": "Перша покупка ріпи", 12 | "description": "Ви купуєте ріпу від Дейзі Мей на своєму острові вперше?(Це має вплив на модель ціни)", 13 | "yes": "Так", 14 | "no": "Ні" 15 | }, 16 | "patterns": { 17 | "title": "Попередня модель ціни", 18 | "description": "Яка модель ціни була попереднього тижня?(Це має вплив на модель ціни)", 19 | "pattern": "Модель", 20 | "all": "Всі моделі", 21 | "decreasing": "Спадаюча", 22 | "fluctuating": "Нестабільна", 23 | "unknown": "Не знаю", 24 | "large-spike": "Великий скачок", 25 | "small-spike": "Малий скачок" 26 | }, 27 | "prices": { 28 | "description": "Які ціни ріпи були на Вашому острові цього тижня?", 29 | "open": { 30 | "am": "Перед обідом - від 8:00 до 11:59", 31 | "pm": "Після обіду - від 12:00 до 22:00" 32 | }, 33 | "copy-permalink": "Копіювати посилання", 34 | "permalink-copied": "Посилання скопійоване!", 35 | "reset": "Перезапустити Turnip Prophet", 36 | "reset-warning": "Ви впевнені що хочете стерти всі поля?\n\nВтрачені дані неможливо повернути!" 37 | }, 38 | "weekdays": { 39 | "monday": "Понеділок", 40 | "tuesday": "Вівторок", 41 | "wednesday": "Середа", 42 | "thursday": "Четвер", 43 | "friday": "П'ятниця", 44 | "saturday" : "Субота", 45 | "sunday": "Неділя", 46 | "abr": { 47 | "monday": "Пон.", 48 | "tuesday": "Вівт.", 49 | "wednesday": "Сер.", 50 | "thursday": "Четв.", 51 | "friday": "П'ятн.", 52 | "saturday" : "Суб." 53 | } 54 | }, 55 | "times": { 56 | "morning": "Перед обідом", 57 | "afternoon": "Після обіду" 58 | }, 59 | "output": { 60 | "title": "Результат", 61 | "chance": "% Шансу", 62 | "to": "до", 63 | "minimum": "Гарантований мінімум", 64 | "maximum": "Можливий максимум", 65 | "chart": { 66 | "input": "Вхідна ціна", 67 | "minimum": "Гарантований мінімум", 68 | "maximum": "Можливий максимум" 69 | } 70 | }, 71 | "textbox": { 72 | "description": "Після того, як Ви запишете делілька цін на ріпу, додаток Turnip Prophet підрахує і покаже різні можливі моделі цін на Вашому острові.", 73 | "development": "Цей додаток ще в розробці, але напевно з часом покращиться!", 74 | "thanks": "Цей додаток не був би можливий без праці Ninji, котрий дізнався як саме Тіммі і Томмі оцінюють їх ріпу.", 75 | "support": "Ви можете підтримати, зробити коментар, або внесок до проекту на сторінці GitHub", 76 | "sponsor": "Хочете заспонсорувати розробників проекту? Відвідайте сторінку GitHub і натисніть '❤ Sponsor'", 77 | "contributors-text": "До речі! Не забудьмо подякувати людям, які зробили внесок до проекту!", 78 | "contributors": "Зробили внесок", 79 | "language": "Мова", 80 | "theme": { 81 | "title": "Тема", 82 | "auto": "Автоматична", 83 | "light": "Світла", 84 | "dark": "Темна" 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /locales/id.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "daisy-mae": "Daisy Mae" 4 | }, 5 | "welcome": { 6 | "salutation": "Halo, dan selamat datang di aplikasi Turnip Prophet pada Nook Phone kalian.", 7 | "description": "Aplikasi ini dapat digunakan untuk mencatat harga turnip harian di pulau kalian, tapi kalian harus memasukkan harganya sendiri!", 8 | "conclusion": "Setelah itu, aplikasi Turnip Prophet akan secara ajaib memprediksi harga turnip kalian selama satu minggu ke depan." 9 | }, 10 | "first-time": { 11 | "title": "Pertama Kali Beli", 12 | "description": "Apakah ini pertama kalinya seorang penduduk membeli turnip dari Daisy Mae di pulau kalian sendiri?(Akan memengaruhi pola harga)", 13 | "yes": "Iya", 14 | "no": "Tidak" 15 | }, 16 | "patterns": { 17 | "title": "Pola Sebelumnya", 18 | "description": "Bagaimana bentuk pola harga turnip pada minggu sebelumnya?(Akan memengaruhi pola harga)", 19 | "pattern": "Pola", 20 | "all": "Semua pola", 21 | "decreasing": "Menurun", 22 | "fluctuating": "Fluktuatif", 23 | "unknown": "Tidak tahu", 24 | "large-spike": "Peningkatan Tajam", 25 | "small-spike": "Peningkatan Kecil" 26 | }, 27 | "prices": { 28 | "description": "Berapa harga turnip di pulau kalian minggu ini?", 29 | "open": { 30 | "am": "AM - 8.00 pagi sampai 11:59 siang", 31 | "pm": "PM - 12.00 siang sampai 22.00 malam" 32 | }, 33 | "copy-permalink": "Salin tautan", 34 | "permalink-copied": "Tautan telah disalin!", 35 | "reset": "Reset Turnip Prophet", 36 | "reset-warning": "Apakah kamu yakin ingin me-reset semua kolom?\n\nSetelah di-reset, tidak dapat dikembalikan lagi!" 37 | }, 38 | "weekdays": { 39 | "monday": "Senin", 40 | "tuesday": "Selasa", 41 | "wednesday": "Rabu", 42 | "thursday": "Kamis", 43 | "friday": "Jumat", 44 | "saturday" : "Sabtu", 45 | "sunday": "Minggu", 46 | "abr": { 47 | "monday": "Sen", 48 | "tuesday": "Sel", 49 | "wednesday": "Rab", 50 | "thursday": "Kam", 51 | "friday": "Jum", 52 | "saturday" : "Sab" 53 | } 54 | }, 55 | "times": { 56 | "morning": "AM", 57 | "afternoon": "PM" 58 | }, 59 | "output": { 60 | "title": "Output", 61 | "chance": "% Peluang", 62 | "to": "-", 63 | "minimum": "Jaminan Minimal", 64 | "maximum": "Potensi Maksimal", 65 | "chart": { 66 | "input": "Input Harga", 67 | "minimum": "Jaminan Minimal", 68 | "maximum": "Potensi Maksimal" 69 | } 70 | }, 71 | "textbox": { 72 | "description": "Setelah kalian mendaftarkan harga turnip-nya, Turnip Prophet akan melakukan perhitungan dan menampilkan beberapa kemungkinan dengan pola yang berbeda yang mungkin terjadi di pulau kalian.", 73 | "development": "Aplikasi ini masih dalam tahap pengembangan, tapi akan selalu bertambah baik seiring dengan berjalannya waktu!", 74 | "thanks": "Aplikasi ini tidak akan terwujud tanpa adanya hasil kerja Ninji yang menemukan cara bagaimana Timmy dan Tommy menentukan harga turnip.", 75 | "support": "Dukungan, komentar, dan kontribusi dapat dilakukan melalui GitHub", 76 | "sponsor": "Ingin mensponsori pengembang di balik proyek ini? Silakan menuju halaman GitHub dan klik '❤ Sponsor'", 77 | "contributors-text": "Oh! Jangan lupa juga untuk berterima kasih pada semuanya yang telah berkontribusi sejauh ini!", 78 | "contributors": "Kontributor", 79 | "language": "Bahasa" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /locales/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "daisy-mae": "Jorna" 4 | }, 5 | "welcome": { 6 | "salutation": "Hallo und Willkommen bei der Turnip Prophet App auf deinem Nook Phone.", 7 | "description": "Mit dieser App kannst du die Rübenpreise deiner Insel täglich verfolgen, aber du musst die Preise selbst eingeben!", 8 | "conclusion": "Danach wird die Turnip Prophet App magisch deine Rübenpreise vorhersagen, welche du den Rest der Woche haben wirst." 9 | }, 10 | "first-time": { 11 | "title": "Erstmaliger Einkäufer", 12 | "description": "Kaufst du zum ersten Mal Rüben von Jorna auf deiner Insel? (Dies beeinflusst dein Verkaufsmuster)", 13 | "yes": "Ja", 14 | "no": "Nein" 15 | }, 16 | "patterns": { 17 | "title": "Vorheriges Verkaufsmuster", 18 | "description": "Wie war das Verkaufsmuster der letzten Woche? (Dies beeinflusst dein Verkaufsmuster)", 19 | "pattern": "Verkaufsmuster", 20 | "all": "Alle Verkaufsmuster", 21 | "decreasing": "Absteigend", 22 | "fluctuating": "Schwankend", 23 | "unknown": "Ich weiß nicht", 24 | "large-spike": "Stark Ansteigend", 25 | "small-spike": "Leicht Ansteigend" 26 | }, 27 | "prices": { 28 | "description": "Wie hoch war der Preis für Rüben diese Woche auf deiner Insel?", 29 | "open": { 30 | "am": "Vorm. (Vormittag) - 8:00 Uhr bis 11:59 Uhr", 31 | "pm": "Nachm. (Nachmittag) - 12:00 Uhr bis 22:00 Uhr" 32 | }, 33 | "copy-permalink": "Seite teilen", 34 | "permalink-copied": "Seitenlink kopiert!", 35 | "reset": "Eingegebene Daten zurücksetzen", 36 | "reset-warning": "Bist du dir sicher, dass du deine eingegebenen Daten zurücksetzen möchtest?\n\nDies kann nicht rückgängig gemacht werden!" 37 | }, 38 | "weekdays": { 39 | "monday": "Montag", 40 | "tuesday": "Dienstag", 41 | "wednesday": "Mittwoch", 42 | "thursday": "Donnerstag", 43 | "friday": "Freitag", 44 | "saturday" : "Samstag", 45 | "sunday": "Sonntag", 46 | "abr": { 47 | "monday": "Mo", 48 | "tuesday": "Di", 49 | "wednesday": "Mi", 50 | "thursday": "Do", 51 | "friday": "Fr", 52 | "saturday" : "Sa" 53 | } 54 | }, 55 | "times": { 56 | "morning": "Vorm.", 57 | "afternoon": "Nachm." 58 | }, 59 | "output": { 60 | "title": "Berechnung", 61 | "chance": "% Chance", 62 | "to": "bis", 63 | "minimum": "Garantiertes Minimum", 64 | "maximum": "Potentielles Maximum", 65 | "chart": { 66 | "input": "Eingegebener Preis", 67 | "minimum": "Garantiertes Minimum", 68 | "maximum": "Potentielles Maximum" 69 | } 70 | }, 71 | "textbox": { 72 | "description": "Nachdem du einige Rübenpreise eingegeben hast, benutzt der Turnip Prophet etwas Magie und zeigt dir die verschiedenen möglichen Verkaufsmuster an, die auf deiner Insel auftreten können.", 73 | "development": "Diese App befindet sich noch in der Entwicklung, wird sich aber mit der Zeit verbessern!", 74 | "thanks": "Nichts von all dem wäre möglich gewesen, ohne dass Ninji herausgefunden hätte, wie Nepp und Schlepp ihre Rübenpreise kalkulieren.", 75 | "support": "Hilfe, Kommentare und Beiträge sind auffindbar über GitHub.com (nur in Englisch).", 76 | "sponsor": "Möchtest du die Entwickler hinter diesem Projekt unterstützen? Gehe zu GitHub.com und klicke auf den '❤ Sponsor' Button für mehr Informationen.", 77 | "contributors-text": "Oh! Und vergessen wir nicht, denen zu danken, die bis jetzt dazu beigetragen haben!", 78 | "contributors": "Mitwirkende", 79 | "language": "Sprache" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /locales/hu.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "daisy-mae": "Daisy Mae" 4 | }, 5 | "welcome": { 6 | "salutation": "Üdvözöllek a Nookfonod Turnip Prophet alkalmazásában!", 7 | "description": "Ezzel az alkalmazással követheted a szigeted naponta változó retekárait, viszont az árakat Neked kell megadnod!", 8 | "conclusion": "Az árak megadása után a Turnip Prophet varázslatosan megjósolja a hét elkövetkezendő árait." 9 | }, 10 | "first-time": { 11 | "title": "Első vásárlás", 12 | "description": "Most vásárol valaki először retket Daisy Mae-től a Te szigeteden?(Ez hatással van a mintádra)", 13 | "yes": "Igen", 14 | "no": "Nem" 15 | }, 16 | "patterns": { 17 | "title": "Előző minta", 18 | "description": "Milyen volt az előző hét mintája?(Ez hatással van a mintádra)", 19 | "pattern": "Minta", 20 | "all": "Minden minta", 21 | "decreasing": "Csökkenő", 22 | "fluctuating": "Ingadozó", 23 | "unknown": "Nem tudom", 24 | "large-spike": "Nagy kiugrás", 25 | "small-spike": "Kis kiugrás" 26 | }, 27 | "prices": { 28 | "description": "Mennyiért vettél ezen a héten retket?", 29 | "open": { 30 | "am": "Délelőtt - 08:00-tól 11:59-ig", 31 | "pm": "Délután - 12:00-től 22:00-ig" 32 | }, 33 | "copy-permalink": "Árak megosztása", 34 | "permalink-copied": "Hivatkozás kimásolva!", 35 | "reset": "Mezők kiürítése", 36 | "reset-warning": "Biztosan ki akarsz üríteni minden mezőt?\n\nEz a művelet nem vonható vissza!" 37 | }, 38 | "weekdays": { 39 | "monday": "Hétfő", 40 | "tuesday": "Kedd", 41 | "wednesday": "Szerda", 42 | "thursday": "Csütörtök", 43 | "friday": "Péntek", 44 | "saturday" : "Szombat", 45 | "sunday": "Vasárnap", 46 | "abr": { 47 | "monday": "H", 48 | "tuesday": "K", 49 | "wednesday": "Sze", 50 | "thursday": "Cs", 51 | "friday": "P", 52 | "saturday" : "Szo" 53 | } 54 | }, 55 | "times": { 56 | "morning": "de.", 57 | "afternoon": "du." 58 | }, 59 | "output": { 60 | "title": "Eredmény", 61 | "chance": "% Esély", 62 | "to": "-", 63 | "minimum": "Garantált minimum", 64 | "maximum": "Lehetséges maximum", 65 | "chart": { 66 | "input": "Megadott ár", 67 | "minimum": "Garantált minimum", 68 | "maximum": "Lehetséges maximum" 69 | } 70 | }, 71 | "textbox": { 72 | "description": "Miután megadtál néhány árat, a Turnip Prophet kiszámolja és megjeleníti az összes lehetséges mintát ami a héten megjelenhet.", 73 | "development": "Ez az alkalmazás fejlesztés alatt áll, de idővel javulni fog!", 74 | "thanks": "Ez nem lett volna lehetséges Ninji munkája nélkül, aki megfejtette, hogy Timmy és Tommy hogyan árazzák a retket.", 75 | "support": "Minden hibajelentést, megjegyzést és hozzájárulást a GitHub-on várunk.", 76 | "sponsor": "Szeretnéd szponzorálni a projekt fejlesztőit? Menj a GitHub oldalra és nyomd meg a „❤ Sponsor” gombot!", 77 | "contributors-text": "Ó, és ne felejtsünk el köszönetet mondani az eddigi hozzájárulóknak!", 78 | "contributors": "Hozzájárulók", 79 | "language": "Nyelv", 80 | "theme": { 81 | "title": "Téma", 82 | "auto": "Automatikus", 83 | "light": "Világos", 84 | "dark": "Sötét" 85 | } 86 | }, 87 | "errors": { 88 | "impossible-values": "Hoppá! Valami nem stimmel a jóslatokkal. Ellenőrizd a bevitt adatokat.", 89 | "github": "Ha az árak megfelelőek és nem időutaztál, kérjük jelezd a hibát." 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /locales/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "daisy-mae": "Daisy Mae" 4 | }, 5 | "welcome": { 6 | "salutation": "Hello, and welcome to the Turnip Prophet app on your Nook Phone.", 7 | "description": "This app lets you track your island's turnip prices daily, but you'll have to put the prices in yourself!", 8 | "conclusion": "After that, the Turnip Prophet app will magically predict the turnip prices you'll have for the rest of the week." 9 | }, 10 | "first-time": { 11 | "title": "First-Time Buyer", 12 | "description": "Is this the first time a resident is buying turnips from Daisy Mae on your own island?(This affects your pattern)", 13 | "yes": "Yes", 14 | "no": "No" 15 | }, 16 | "patterns": { 17 | "title": "Previous Pattern", 18 | "description": "What was last week's turnip price pattern?(This affects your pattern)", 19 | "pattern": "Pattern", 20 | "all": "All patterns", 21 | "decreasing": "Decreasing", 22 | "fluctuating": "Fluctuating", 23 | "unknown": "I don't know", 24 | "large-spike": "Large Spike", 25 | "small-spike": "Small Spike" 26 | }, 27 | "prices": { 28 | "description": "What was the price of turnips this week on your island?", 29 | "open": { 30 | "am": "AM - 8:00 am to 11:59 am", 31 | "pm": "PM - 12:00 pm to 10:00 pm" 32 | }, 33 | "copy-permalink": "Copy permalink", 34 | "permalink-copied": "Permalink copied!", 35 | "reset": "Reset Turnip Prophet", 36 | "reset-warning": "Are you sure you want to reset all fields?\n\nThis cannot be undone!" 37 | }, 38 | "weekdays": { 39 | "monday": "Monday", 40 | "tuesday": "Tuesday", 41 | "wednesday": "Wednesday", 42 | "thursday": "Thursday", 43 | "friday": "Friday", 44 | "saturday" : "Saturday", 45 | "sunday": "Sunday", 46 | "abr": { 47 | "monday": "Mon", 48 | "tuesday": "Tue", 49 | "wednesday": "Wed", 50 | "thursday": "Thu", 51 | "friday": "Fri", 52 | "saturday" : "Sat" 53 | } 54 | }, 55 | "times": { 56 | "morning": "AM", 57 | "afternoon": "PM" 58 | }, 59 | "output": { 60 | "title": "Output", 61 | "chance": "% Chance", 62 | "to": "to", 63 | "minimum": "Guaranteed Minimum", 64 | "maximum": "Potential Maximum", 65 | "chart": { 66 | "input": "Input Price", 67 | "minimum": "Guaranteed Minimum", 68 | "maximum": "Potential Maximum" 69 | } 70 | }, 71 | "textbox": { 72 | "description": "After you've listed some turnip prices, the Turnip Prophet will run some numbers and display the different possible patterns that your island may experience.", 73 | "development": "This app is still in development, but will improve over time!", 74 | "thanks": "None of this would have been possible without Ninji's work figuring out just how Timmy and Tommy value their turnips.", 75 | "support": "Support, comments and contributions are available through GitHub", 76 | "sponsor": "Want to sponsor the developers behind this project? Go to the GitHub page and click '❤ Sponsor'", 77 | "contributors-text": "Oh! And let's not forget to thank those who have contributed so far!", 78 | "contributors": "Contributors", 79 | "language": "Language", 80 | "theme": { 81 | "title": "Theme", 82 | "auto": "Automatic", 83 | "light": "Light", 84 | "dark": "Dark" 85 | } 86 | }, 87 | "errors": { 88 | "impossible-values": "Oops! There seems to be an error with your turnip predictions. Please check your inputs.", 89 | "github": "If your turnip prices are correct and you have not time traveled, please submit a report." 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /locales/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "daisy-mae": "Porcelette" 4 | }, 5 | "welcome": { 6 | "salutation": "Bonjour et bienvenue sur l'application du Turnip Prophet de ton Nook Phone.", 7 | "description": "Cette appli te permet de garder un œil quotidien sur le cours du navet de ton île, en le renseignant ici par toi-même !", 8 | "conclusion": "Une fois les prix renseignés, l'appli du Turnip Prophet va magiquement prédire le cours du navet de ton île pour le reste de la semaine." 9 | }, 10 | "first-time": { 11 | "title": "Premier Achat", 12 | "description": "Est-ce la toute première fois que t'achètes des navets à Porcelette sur ton île ?(Cela influencera le type de ta courbe actuelle)", 13 | "yes": "Oui", 14 | "no": "Non" 15 | }, 16 | "patterns": { 17 | "title": "Courbe du cours précédent", 18 | "description": "De quel type était la courbe de ton cours du navet la semaine dernière ?(Cela influencera le type de ta courbe actuelle)", 19 | "pattern": "Type de Courbe", 20 | "all": "Tous les types", 21 | "decreasing": "Décroissante", 22 | "fluctuating": "Variable", 23 | "unknown": "Je ne sais pas", 24 | "large-spike": "Grand Pic", 25 | "small-spike": "Petit Pic" 26 | }, 27 | "prices": { 28 | "description": "À quel prix Porcelette vendait ses navets sur ton île cette semaine ?", 29 | "open": { 30 | "am": "Matin (AM) - de 8:00 à 11:59", 31 | "pm": "Après-midi (PM) - de 12:00 à 22:00" 32 | }, 33 | "copy-permalink": "Copier le permalien", 34 | "permalink-copied": "Permalien copié !", 35 | "reset": "Réinitialiser les données", 36 | "reset-warning": "Es-tu sûr·e de vouloir réinitialiser tous les champs ?\n\nCe choix est définitif !" 37 | }, 38 | "weekdays": { 39 | "monday": "Lundi", 40 | "tuesday": "Mardi", 41 | "wednesday": "Mercredi", 42 | "thursday": "Jeudi", 43 | "friday": "Vendredi", 44 | "saturday" : "Samedi", 45 | "sunday": "Dimanche", 46 | "abr": { 47 | "monday": "Lun", 48 | "tuesday": "Mar", 49 | "wednesday": "Mer", 50 | "thursday": "Jeu", 51 | "friday": "Ven", 52 | "saturday" : "Sam" 53 | } 54 | }, 55 | "times": { 56 | "morning": "AM", 57 | "afternoon": "PM" 58 | }, 59 | "output": { 60 | "title": "Résultats", 61 | "chance": "% Chance", 62 | "to": "à", 63 | "minimum": "Minimum Garanti", 64 | "maximum": "Maximum Potentiel", 65 | "chart": { 66 | "input": "Prix renseigné", 67 | "minimum": "Minimum Garanti", 68 | "maximum": "Maximum Potentiel" 69 | } 70 | }, 71 | "textbox": { 72 | "description": "Après avoir renseigné quelques prix, le Turnip Prophet fera des calculs et affichera les différents types possibles pour les courbes que ton île pourrait avoir.", 73 | "development": "Cette appli est en développement, mais elle s'améliore jour après jour !", 74 | "thanks": "Rien de tout ça n'aurait pu être possible sans le travail de Ninji et son analyse sur comment Méli et Mélo déterminent la valeur des Navets.", 75 | "support": "Aide, commentaires et contributions sont disponibles via GitHub", 76 | "sponsor": "T'aimerais sponsoriser les développeurs derrière ce projet ? Alors va sur la page GitHub du projet et clique sur '❤ Sponsor'", 77 | "contributors-text": "Oh ! Et n'oublions pas de remercier toutes les personnes ayant contribué jusqu'à maintenant !", 78 | "contributors": "Contributeurs", 79 | "language": "Langue", 80 | "theme": { 81 | "title": "Thème", 82 | "auto": "Automatique", 83 | "light": "Clair", 84 | "dark": "Sombre" 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /locales/ph.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "daisy-mae": "Daisy Mae" 4 | }, 5 | "welcome": { 6 | "salutation": "Kumusta, at maligayang pagdating sa Turnip Prophet app sa iyong Nook Phone.", 7 | "description": "Sa pamamagitan ng app na ito, puwede mo na subaybayan ang mga presyo ng turnip ng iyong isla araw-araw, ngunit kailangan mong ilagay ang mga presyo sa iyong sarili!", 8 | "conclusion": "Pagkatapos nito, ang Turnip Prophet app ay huhulaan ang magiging presyo ng singil sa iyo para sa natitirang linggo, sa pamamagitan ng mahika!" 9 | }, 10 | "first-time": { 11 | "title": "Unang beses na Mamimili", 12 | "description": "Ito ba ang unang pagkakataon na ang isang residente ay namimili ng mga turnip mula kay Daisy Mae sa iyong sariling isla?(Nakakaapekto ito sa iyong pattern)", 13 | "yes": "Oo", 14 | "no": "Hindi" 15 | }, 16 | "patterns": { 17 | "title": "Nakaraang Pattern", 18 | "description": "Ano ang pattern ng presyo ng turnip noong nakaraang linggo?(Nakakaapekto ito sa iyong pattern)", 19 | "pattern": "Pattern", 20 | "all": "Lahat ng mga pattern", 21 | "decreasing": "Pababa", 22 | "fluctuating": "Taas-Baba", 23 | "unknown": "Hindi ko alam", 24 | "large-spike": "Malaki na Pag-taas", 25 | "small-spike": "Maliit na Pag-taas" 26 | }, 27 | "prices": { 28 | "description": "Ano ang presyo ng mga turnip sa linggong ito sa iyong isla?", 29 | "open": { 30 | "am": "AM - 8:00 am - 11:59 am", 31 | "pm": "PM - 12:00 pm - 10:00 pm" 32 | }, 33 | "copy-permalink": "Kopyahin ang permalink", 34 | "permalink-copied": "Nakopya ang permalink!", 35 | "reset": "I-reset ang Turnip Prophet", 36 | "reset-warning": "Sigurado ka bang nais mong i-reset ang lahat ng mga patlang?\n\nHindi na ito maibabalik sa dati!" 37 | }, 38 | "weekdays": { 39 | "monday": "Lunes", 40 | "tuesday": "Martes", 41 | "wednesday": "Miyerkules", 42 | "thursday": "Huwebes", 43 | "friday": "Biyernes", 44 | "saturday" : "Sabado", 45 | "sunday": "Linggo", 46 | "abr": { 47 | "monday": "Lunes", 48 | "tuesday": "Martes", 49 | "wednesday": "Miyerkules", 50 | "thursday": "Huwebes", 51 | "friday": "Biyernes", 52 | "saturday" : "Sabado" 53 | } 54 | }, 55 | "times": { 56 | "morning": "AM", 57 | "afternoon": "PM" 58 | }, 59 | "output": { 60 | "title": "Resulta", 61 | "chance": "% ng Tiyansa", 62 | "to": "-", 63 | "minimum": "Garantisadong Minimum", 64 | "maximum": "Potensyal na Maximum", 65 | "chart": { 66 | "input": "Na-input na Presyo", 67 | "minimum": "Garantisadong Minimum", 68 | "maximum": "Potensyal na Maximum" 69 | } 70 | }, 71 | "textbox": { 72 | "description": "Pagkatapos mong mailista ang ilang mga presyo ng turnip, kakalkulahin ng Turnip Prophet ang mga numero at lilitaw ang iba't ibang mga posibleng pattern na maaaring maranasan ng iyong isla.", 73 | "development": "Ang app na ito ay nasa pag-unlad pa rin, ngunit mapapabuti sa paglipas ng panahon!", 74 | "thanks": "Lahat nang ito ay hindi posible kung wala ang tulong ni Ninji sa pag-alam kung paano pinepresyuhan ni Timmy at Tommy ang kanilang mga turnip.", 75 | "support": "Pumunta lamang sa GitHub para sa suporta, puna, at kontribusyon.", 76 | "sponsor": "Nais mo ba i-sponsor ang mga developer sa likod ng proyektong ito? Pumunta lamang sa GitHub page at i-click ang '❤ Sponsor'", 77 | "contributors-text": "Siya nga pala! Huwag din natin kalimutan na pasalamatan ang mga nag-bigay ng kanilang kontribusyon sa ngayon!", 78 | "contributors": "Contributors", 79 | "language": "Wika", 80 | "theme": { 81 | "title": "Tema", 82 | "auto": "Automatic", 83 | "light": "Light", 84 | "dark": "Dark" 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /locales/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "daisy-mae": "Juliana" 4 | }, 5 | "welcome": { 6 | "salutation": "¡Hola! Te damos la bienvenida a la aplicación Turnip Prophet para tu Nookófono.", 7 | "description": "Esta aplicación te permite monitorizar la fluctuación del precio de los nabos en tu isla, ¡pero tendrás que introducir tú manualmente los precios diarios!", 8 | "conclusion": "Una vez hecho, la aplicación Turnip Prophet predecirá mágicamente el precio que tendrán los nabos el resto de la semana." 9 | }, 10 | "first-time": { 11 | "title": "Comprador(a) primerizo(a)", 12 | "description": "¿Ha sido esta la primera vez que compras nabos?(Esta información afectará a tu patrón)", 13 | "yes": "Sí", 14 | "no": "No" 15 | }, 16 | "patterns": { 17 | "title": "Patrón anterior", 18 | "description": "¿Qué patrón describió el precio de los nabos la semana pasada?(Esta información afectará a tu patrón)", 19 | "pattern": "Patrón", 20 | "all": "Todos los patrones", 21 | "decreasing": "Decreciente", 22 | "fluctuating": "Fluctuante", 23 | "unknown": "No lo sé", 24 | "large-spike": "Pico alto", 25 | "small-spike": "Pico moderado" 26 | }, 27 | "prices": { 28 | "description": "¿Cuál fue el precio de los nabos en tu isla esta semana?", 29 | "open": { 30 | "am": "AM - De 8:00 a 11:59", 31 | "pm": "PM - De 12:00 a 22:00" 32 | }, 33 | "copy-permalink": "Copiar permalink", 34 | "permalink-copied": "¡Permalink copiado!", 35 | "reset": "Reiniciar Turnip Prophet", 36 | "reset-warning": "¿Seguro que quieres reiniciar todos los campos?\n\n¡Esto no se puede deshacer!" 37 | }, 38 | "weekdays": { 39 | "monday": "Lunes", 40 | "tuesday": "Martes", 41 | "wednesday": "Miércoles", 42 | "thursday": "Jueves", 43 | "friday": "Viernes", 44 | "saturday" : "Sábado", 45 | "sunday": "Domingo", 46 | "abr": { 47 | "monday": "LU", 48 | "tuesday": "MA", 49 | "wednesday": "MI", 50 | "thursday": "JU", 51 | "friday": "VI", 52 | "saturday" : "SA" 53 | } 54 | }, 55 | "times": { 56 | "morning": "AM", 57 | "afternoon": "PM" 58 | }, 59 | "output": { 60 | "title": "Predicción", 61 | "chance": "Probabilidad (%)", 62 | "to": "a", 63 | "minimum": "Mínimo garantizado", 64 | "maximum": "Máximo potencial", 65 | "chart": { 66 | "input": "Precio de entrada", 67 | "minimum": "Mínimo garantizado", 68 | "maximum": "Máximo potencial" 69 | } 70 | }, 71 | "textbox": { 72 | "description": "Cuando introduzcas algunos precios, Turnip Prophet empezará a hacer sus cálculos y te mostrará algunos posibles patrones para el precio de los nabos en tu isla.", 73 | "development": "Esta aplicación está aún en desarrollo, ¡pero la seguiremos mejorando!", 74 | "thanks": "Nada de esto habría sido posible sin el trabajo de Ninji para averiguar cómo Tendo y Nendo valoran los nabos.", 75 | "support": "Para asistencia, comentarios y contribuciones, no dudes en pasarte por GitHub.", 76 | "sponsor": "¿Quieres apoyar a los desarrolladores del proyecto? Visita la página de GitHub y pulsa sobre '❤ Sponsor'.", 77 | "contributors-text": "¡Ah! ¡Y no nos olvidemos de todos los que han puesto su granito de arena hasta ahora!", 78 | "contributors": "Contribuidores", 79 | "language": "Idioma", 80 | "theme": { 81 | "title": "Tema", 82 | "auto": "Automático", 83 | "light": "Claro", 84 | "dark": "Oscuro" 85 | } 86 | }, 87 | "errors": { 88 | "impossible-values": "¡Ups! Parece que hay un error con tus predicciones. Por favor, revisa tus valores introducidos.", 89 | "github": "Si el precio de los nabos es correcto y no has viajado en el tiempo, por favor, informa sobre el problema." 90 | } 91 | } -------------------------------------------------------------------------------- /locales/pt-BR.json: -------------------------------------------------------------------------------- 1 | { 2 | "general": { 3 | "daisy-mae": "Daisy Mae" 4 | }, 5 | "welcome": { 6 | "salutation": "Olá, e bem-vindo ao aplicativo Turnip Prophet em seu Nook Phone.", 7 | "description": "Este aplicativo lhe permite acompanhar os preços diários de nabo em sua ilha, porém você terá que colocar os preços você mesmo!", 8 | "conclusion": "Depois disso, o aplicativo Turnip Prophet irá magicamente prever os preços dos nabos que você terá pelo resto da semana." 9 | }, 10 | "first-time": { 11 | "title": "Comprador de primeira viagem", 12 | "description": "Esta é a primeira vez que você compra nabos de Daisy Mae em sua ilha? (Isso afeta seu padrão)", 13 | "yes": "Sim", 14 | "no": "Não" 15 | }, 16 | "patterns": { 17 | "title": "Padrão Anterior", 18 | "description": "Qual foi o padrão de preços de nabo da semana passada? (Isso afeta seu padrão)", 19 | "pattern": "Padrão", 20 | "all": "Todos padrões", 21 | "decreasing": "Diminuindo", 22 | "fluctuating": "Flutuante", 23 | "unknown": "Eu não sei", 24 | "large-spike": "Grande Pico", 25 | "small-spike": "Pequeno Pico" 26 | }, 27 | "prices": { 28 | "description": "Qual foi o preço dos nabos esta semana em sua ilha?", 29 | "open": { 30 | "am": "AM - 8:00 am até 11:59 am", 31 | "pm": "PM - 12:00 pm até 10:00 pm" 32 | }, 33 | "copy-permalink": "Copiar permalink", 34 | "permalink-copied": "Permalink copiado!", 35 | "reset": "Redefinir Turnip Prophet", 36 | "reset-warning": "Tem certeza de que deseja redefinir todos os campos?\n\nIsso não pode ser desfeito!" 37 | }, 38 | "weekdays": { 39 | "monday": "Segunda-feira", 40 | "tuesday": "Terça-feira", 41 | "wednesday": "Quarta-feira", 42 | "thursday": "Quinta-feira", 43 | "friday": "Sexta-feira", 44 | "saturday" : "Sábado", 45 | "sunday": "Domingo", 46 | "abr": { 47 | "monday": "Seg", 48 | "tuesday": "Ter", 49 | "wednesday": "Qua", 50 | "thursday": "Qui", 51 | "friday": "Sex", 52 | "saturday" : "Sab" 53 | } 54 | }, 55 | "times": { 56 | "morning": "AM", 57 | "afternoon": "PM" 58 | }, 59 | "output": { 60 | "title": "Resultado", 61 | "chance": "% Chance", 62 | "to": "to", 63 | "minimum": "Mínimo Garantido", 64 | "maximum": "Potencial Máximo", 65 | "chart": { 66 | "input": "Preço de entrada", 67 | "minimum": "Mínimo Garantido", 68 | "maximum": "Potencial Máximo" 69 | } 70 | }, 71 | "textbox": { 72 | "description": "Depois de listar alguns preços de nabo, o Turnip Prophet irá calcular alguns números e exibirá os diferentes padrões possíveis que sua ilha pode experienciar.", 73 | "development": "Este aplicativo ainda está em desenvolvimento, mas melhorará com o tempo!", 74 | "thanks": "Nada disso seria possível sem o trabalho de Ninji's descobrindo como Timmy e Tommy valorizam seus nabos.", 75 | "support": "Suporte, comentários e contribuições estão disponíveis através do GitHub", 76 | "sponsor": "Quer patrocinar os desenvolvedores por trás deste projeto? Vá para a página de GitHub e clique em '❤ Sponsor'.", 77 | "contributors-text": "Oh! E não vamos nos esquecer de agradecer àqueles que contribuíram até agora!", 78 | "contributors": "Contribuidores", 79 | "language": "Linguagem", 80 | "theme": { 81 | "title": "Tema", 82 | "auto": "Automático", 83 | "light": "Claro", 84 | "dark": "Escuro" 85 | } 86 | }, 87 | "errors": { 88 | "impossible-values": "Opa! Parece haver um erro com suas previsões. Por favor, verifique os valores introduzidos.", 89 | "github": "Se seus preços dos nabos estão corretos e você não viajou no tempo, por favor, relate o problema." 90 | } 91 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /js/scripts.js: -------------------------------------------------------------------------------- 1 | //Reusable Fields 2 | const getSellFields = function () { 3 | let fields = []; 4 | var now = new Date(); 5 | for (var i = 2; i < 14; i++) { 6 | fields.push($("#sell_" + i)[0]); 7 | if (i == now.getDay() * 2 + (now.getHours() >= 12 ? 1 : 0)) { 8 | fields[fields.length - 1].classList.add("now"); 9 | } 10 | } 11 | if (now.getDay() == 0) { 12 | buy_input[0].classList.add("now"); 13 | } 14 | return fields; 15 | }; 16 | 17 | const getFirstBuyRadios = function () { 18 | return [ 19 | $("#first-time-radio-no")[0], 20 | $("#first-time-radio-yes")[0] 21 | ]; 22 | }; 23 | 24 | const getPreviousPatternRadios = function () { 25 | return [ 26 | $("#pattern-radio-unknown")[0], 27 | $("#pattern-radio-fluctuating")[0], 28 | $("#pattern-radio-small-spike")[0], 29 | $("#pattern-radio-large-spike")[0], 30 | $("#pattern-radio-decreasing")[0] 31 | ]; 32 | }; 33 | 34 | const getCheckedRadio = function (radio_array) { 35 | return radio_array.find(radio => radio.checked === true).value; 36 | }; 37 | 38 | const checkRadioByValue = function (radio_array, value) { 39 | if (value === null) { 40 | return; 41 | } 42 | value = value.toString(); 43 | radio_array.find(radio => radio.value == value).checked = true; 44 | }; 45 | 46 | const state = { 47 | initialized: false, 48 | }; 49 | 50 | const buy_input = $("#buy"); 51 | const sell_inputs = getSellFields(); 52 | const first_buy_radios = getFirstBuyRadios(); 53 | const previous_pattern_radios = getPreviousPatternRadios(); 54 | const permalink_input = $('#permalink-input'); 55 | const permalink_button = $('#permalink-btn'); 56 | const snackbar = $('#snackbar'); 57 | 58 | //Functions 59 | const fillFields = function (prices, first_buy, previous_pattern) { 60 | checkRadioByValue(first_buy_radios, first_buy); 61 | checkRadioByValue(previous_pattern_radios, previous_pattern); 62 | 63 | buy_input.focus(); 64 | buy_input.val(prices[0] || ''); 65 | buy_input.blur(); 66 | const sell_prices = prices.slice(2); 67 | 68 | sell_prices.forEach((price, index) => { 69 | if (!price) { 70 | return; 71 | } else { 72 | const element = $("#sell_" + (index + 2)); 73 | element.focus(); 74 | element.val(price); 75 | element.blur(); 76 | } 77 | }); 78 | }; 79 | 80 | const initialize = function () { 81 | try { 82 | const previous = getPrevious(); 83 | const first_buy = previous[0]; 84 | const previous_pattern = previous[1]; 85 | const prices = previous[2]; 86 | if (prices === null) { 87 | fillFields([], first_buy, previous_pattern); 88 | } else { 89 | fillFields(prices, first_buy, previous_pattern); 90 | } 91 | } catch (e) { 92 | console.error(e); 93 | } 94 | 95 | $(document).trigger("input"); 96 | 97 | $("#permalink-btn").on("click", copyPermalink); 98 | 99 | $("#reset").on("click", function () { 100 | if (window.confirm(i18next.t("prices.reset-warning"))) { 101 | sell_inputs.forEach(input => input.value = ''); 102 | fillFields([], false, -1); 103 | update(); 104 | } 105 | }); 106 | 107 | console.log('finished initializing'); 108 | state.initialized = true; 109 | }; 110 | 111 | const updateLocalStorage = function (prices, first_buy, previous_pattern) { 112 | try { 113 | if (prices.length !== 14) throw "The data array needs exactly 14 elements to be valid"; 114 | localStorage.setItem("sell_prices", JSON.stringify(prices)); 115 | localStorage.setItem("first_buy", JSON.stringify(first_buy)); 116 | localStorage.setItem("previous_pattern", JSON.stringify(previous_pattern)); 117 | } catch (e) { 118 | console.error(e); 119 | } 120 | }; 121 | 122 | const isEmpty = function (arr) { 123 | const filtered = arr.filter(value => value !== null && value !== '' && !isNaN(value)); 124 | return filtered.length == 0; 125 | }; 126 | 127 | const getFirstBuyStateFromQuery = function (param) { 128 | try { 129 | const params = new URLSearchParams(window.location.search.substr(1)); 130 | const firstbuy_str = params.get(param); 131 | 132 | if (firstbuy_str == null) { 133 | return null; 134 | } 135 | 136 | firstbuy = null; 137 | if (firstbuy_str == "1" || firstbuy_str == "yes" || firstbuy_str == "true") { 138 | firstbuy = true; 139 | } else if (firstbuy_str == "0" || firstbuy_str == "no" || firstbuy_str == "false") { 140 | firstbuy = false; 141 | } 142 | 143 | return firstbuy; 144 | 145 | } catch (e) { 146 | return null; 147 | } 148 | }; 149 | 150 | const getFirstBuyStateFromLocalstorage = function () { 151 | return JSON.parse(localStorage.getItem('first_buy')); 152 | }; 153 | 154 | const getPreviousPatternStateFromLocalstorage = function () { 155 | return JSON.parse(localStorage.getItem('previous_pattern')); 156 | }; 157 | 158 | const getPreviousPatternStateFromQuery = function (param) { 159 | try { 160 | const params = new URLSearchParams(window.location.search.substr(1)); 161 | const pattern_str = params.get(param); 162 | 163 | if (pattern_str == null) { 164 | return null; 165 | } 166 | 167 | if (pattern_str == "0" || pattern_str == "fluctuating") { 168 | pattern = 0; 169 | } else if (pattern_str == "1" || pattern_str == "large-spike") { 170 | pattern = 1; 171 | } else if (pattern_str == "2" || pattern_str == "decreasing") { 172 | pattern = 2; 173 | } else if (pattern_str == "3" || pattern_str == "small-spike") { 174 | pattern = 3; 175 | } else { 176 | pattern = -1; 177 | } 178 | 179 | return pattern; 180 | 181 | } catch (e) { 182 | return null; 183 | } 184 | }; 185 | 186 | const getPricesFromLocalstorage = function () { 187 | try { 188 | const sell_prices = JSON.parse(localStorage.getItem("sell_prices")); 189 | 190 | if (!Array.isArray(sell_prices) || sell_prices.length !== 14) { 191 | return null; 192 | } 193 | 194 | return sell_prices; 195 | } catch (e) { 196 | return null; 197 | } 198 | }; 199 | 200 | const getPricesFromQuery = function (param) { 201 | try { 202 | const params = new URLSearchParams(window.location.search.substr(1)); 203 | const sell_prices = params.get(param).split(".").map((x) => parseInt(x, 10)); 204 | 205 | if (!Array.isArray(sell_prices)) { 206 | return null; 207 | } 208 | 209 | // Parse the array which is formatted like: [price, M-AM, M-PM, T-AM, T-PM, W-AM, W-PM, Th-AM, Th-PM, F-AM, F-PM, S-AM, S-PM, Su-AM, Su-PM] 210 | // due to the format of local storage we need to double up the price at the start of the array. 211 | sell_prices.unshift(sell_prices[0]); 212 | 213 | // This allows us to fill out the missing fields at the end of the array 214 | for (let i = sell_prices.length; i < 14; i++) { 215 | sell_prices.push(0); 216 | } 217 | 218 | return sell_prices; 219 | } catch (e) { 220 | return null; 221 | } 222 | }; 223 | 224 | const getPreviousFromQuery = function () { 225 | /* Check if valid prices are entered. Exit immediately if not. */ 226 | const prices = getPricesFromQuery("prices"); 227 | if (prices == null) { 228 | return null; 229 | } 230 | 231 | console.log("Using data from query."); 232 | window.populated_from_query = true; 233 | return [ 234 | getFirstBuyStateFromQuery("first"), 235 | getPreviousPatternStateFromQuery("pattern"), 236 | prices 237 | ]; 238 | }; 239 | 240 | const getPreviousFromLocalstorage = function () { 241 | return [ 242 | getFirstBuyStateFromLocalstorage(), 243 | getPreviousPatternStateFromLocalstorage(), 244 | getPricesFromLocalstorage() 245 | ]; 246 | }; 247 | 248 | 249 | /** 250 | * Gets previous values. First tries to parse parameters, 251 | * if none of them match then it looks in local storage. 252 | * @return {[first time, previous pattern, prices]} 253 | */ 254 | const getPrevious = function () { 255 | return getPreviousFromQuery() || getPreviousFromLocalstorage(); 256 | }; 257 | 258 | const getSellPrices = function () { 259 | //Checks all sell inputs and returns an array with their values 260 | return res = sell_inputs.map(function (input) { 261 | return parseInt(input.value || ''); 262 | }); 263 | }; 264 | 265 | const getPriceClass = function(buy_price, max) { 266 | const priceBrackets = [200, 30, 0, -30, -99]; 267 | let diff = max - buy_price; 268 | for(var i=0; i= priceBrackets[i]) { 270 | return "range" + i; 271 | } 272 | } 273 | return ""; 274 | }; 275 | 276 | const displayPercentage = function(fraction) { 277 | if (Number.isFinite(fraction)) { 278 | let percent = fraction * 100; 279 | if (percent >= 1) { 280 | return percent.toPrecision(3) + '%'; 281 | } else if (percent >= 0.01) { 282 | return percent.toFixed(2) + '%'; 283 | } else { 284 | return '<0.01%'; 285 | } 286 | } else { 287 | return '—'; 288 | } 289 | }; 290 | 291 | const hideChart = function() { 292 | $("#output").html(""); 293 | $(".chart-wrapper").hide() 294 | } 295 | 296 | const calculateOutput = function (data, first_buy, previous_pattern) { 297 | if (isEmpty(data)) { 298 | hideChart() 299 | return; 300 | } 301 | let pat_desc = {0:"fluctuating", 1:"large-spike", 2:"decreasing", 3:"small-spike", 4:"all"}; 302 | let output_possibilities = ""; 303 | let predictor = new Predictor(data, first_buy, previous_pattern); 304 | let analyzed_possibilities = predictor.analyze_possibilities(); 305 | if (analyzed_possibilities[0].weekGuaranteedMinimum === Number.POSITIVE_INFINITY) { 306 | hideChart() 307 | $(".error:hidden").show() 308 | return; 309 | } 310 | $(".error:visible").hide() 311 | $(".chart-wrapper:hidden").show() 312 | let buy_price = parseInt(buy_input.val()); 313 | previous_pattern_number = ""; 314 | for (let poss of analyzed_possibilities) { 315 | var out_line = "" + i18next.t("patterns." + pat_desc[poss.pattern_number]) + ""; 316 | const style_price = buy_price || poss.prices[0].min; 317 | if (previous_pattern_number != poss.pattern_number) { 318 | previous_pattern_number = poss.pattern_number; 319 | pattern_count = analyzed_possibilities 320 | .filter(val => val.pattern_number == poss.pattern_number) 321 | .length; 322 | out_line += `${displayPercentage(poss.category_total_probability)}`; 323 | } 324 | out_line += `${displayPercentage(poss.probability)}`; 325 | for (let day of poss.prices.slice(2)) { 326 | let price_class = getPriceClass(style_price, day.max); 327 | if (day.min !== day.max) { 328 | out_line += `${day.min} ${i18next.t("output.to")} ${day.max}`; 329 | } else { 330 | out_line += `${day.min}`; 331 | } 332 | } 333 | 334 | var min_class = getPriceClass(style_price, poss.weekGuaranteedMinimum); 335 | var max_class = getPriceClass(style_price, poss.weekMax); 336 | out_line += `${poss.weekGuaranteedMinimum}${poss.weekMax}`; 337 | output_possibilities += out_line; 338 | } 339 | 340 | $("#output").html(output_possibilities); 341 | 342 | update_chart(data, analyzed_possibilities); 343 | }; 344 | 345 | const generatePermalink = function (buy_price, sell_prices, first_buy, previous_pattern) { 346 | let searchParams = new URLSearchParams(); 347 | let pricesParam = buy_price ? buy_price.toString() : ''; 348 | 349 | if (!isEmpty(sell_prices)) { 350 | const filtered = sell_prices.map(price => isNaN(price) ? '' : price).join('.'); 351 | pricesParam = pricesParam.concat('.', filtered); 352 | } 353 | 354 | if (pricesParam) { 355 | searchParams.append('prices', pricesParam); 356 | } 357 | 358 | if (first_buy) { 359 | searchParams.append('first', true); 360 | } 361 | 362 | if (previous_pattern !== -1) { 363 | searchParams.append('pattern', previous_pattern); 364 | } 365 | 366 | return searchParams.toString() && window.location.origin.concat('?', searchParams.toString()); 367 | }; 368 | 369 | const copyPermalink = function () { 370 | let text = permalink_input[0]; 371 | 372 | permalink_input.show(); 373 | text.select(); 374 | text.setSelectionRange(0, 99999); /* for mobile devices */ 375 | 376 | document.execCommand('copy'); 377 | permalink_input.hide(); 378 | 379 | flashMessage(i18next.t("prices.permalink-copied")); 380 | }; 381 | 382 | const flashMessage = function(message) { 383 | snackbar.text(message); 384 | snackbar.addClass('show'); 385 | 386 | setTimeout(function () { 387 | snackbar.removeClass('show'); 388 | snackbar.text(''); 389 | }, 3000); 390 | }; 391 | 392 | const update = function () { 393 | if(!state.initialized){ 394 | console.log('update function called before initial data load'); 395 | // calls to update before the previous data has been initialized / loaded will reset the data. 396 | return; 397 | } 398 | const sell_prices = getSellPrices(); 399 | const buy_price = parseInt(buy_input.val()); 400 | const first_buy = getCheckedRadio(first_buy_radios) == 'true'; 401 | const previous_pattern = parseInt(getCheckedRadio(previous_pattern_radios)); 402 | 403 | const permalink = generatePermalink(buy_price, sell_prices, first_buy, previous_pattern); 404 | if (permalink) { 405 | permalink_button.show(); 406 | } else { 407 | permalink_button.hide(); 408 | } 409 | permalink_input.val(permalink); 410 | 411 | const prices = [buy_price, buy_price, ...sell_prices]; 412 | 413 | if (!window.populated_from_query) { 414 | updateLocalStorage(prices, first_buy, previous_pattern); 415 | } 416 | 417 | calculateOutput(prices, first_buy, previous_pattern); 418 | }; 419 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Animal Crossing - Turnip Prophet 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 41 | 42 | 43 | 54 | 55 | 56 | 57 | 58 | 59 |
60 |

61 |

62 |

63 |

64 |
65 | 66 |
67 |

Turnip Prophet

68 | 69 |
70 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 |
83 | 84 |
85 | 86 | 87 | 88 |
89 |
90 |
91 |
92 | 93 |
94 | 95 | 96 | 97 | 98 |
99 |
100 |
101 | 102 |
103 |
104 |
105 | 106 |
107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 |
118 |
119 |
120 | 121 |
122 |
123 |
124 | 125 | 126 | 127 |
128 |
129 | 130 | 131 |
132 |
133 |
134 |
135 | 136 | 137 |
138 |
139 | 140 | 141 |
142 |
143 | 144 |
145 |
146 |
147 | 148 | 149 |
150 |
151 | 152 | 153 |
154 |
155 | 156 |
157 |
158 |
159 | 160 | 161 |
162 |
163 | 164 | 165 |
166 |
167 | 168 |
169 |
170 |
171 | 172 | 173 |
174 |
175 | 176 | 177 |
178 |
179 | 180 |
181 |
182 |
183 | 184 | 185 |
186 |
187 | 188 | 189 |
190 |
191 | 192 |
193 |
194 |
195 | 196 | 197 |
198 |
199 | 200 | 201 |
202 |
203 |
204 | 205 | 211 | 212 |
213 | 214 | 215 | 216 |

217 | 218 |
219 |

220 |

221 |

222 |
223 | 224 |
225 | 226 |
227 | 228 |
229 | 230 | 231 | 232 | 233 | 234 | 241 | 248 | 255 | 262 | 269 | 276 | 277 | 278 | 279 | 280 | 281 |
235 |
236 |
237 | 238 | 239 |
240 |
242 |
243 |
244 | 245 | 246 |
247 |
249 |
250 |
251 | 252 | 253 |
254 |
256 |
257 |
258 | 259 | 260 |
261 |
263 |
264 |
265 | 266 | 267 |
268 |
270 |
271 |
272 | 273 | 274 |
275 |
282 |
283 |
284 | 285 |
286 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 |
299 |
300 | 301 | 302 | 303 |
304 |

305 |

306 |

307 |
308 | 309 |
310 |

311 |

312 |

313 |

314 |

:

315 |

316 |
317 |

:

318 |
319 |
320 |

:

321 |
322 |
323 | 324 |
Some text some message...
325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 360 | 361 | 362 | -------------------------------------------------------------------------------- /css/styles.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Raleway:wght@800&family=Varela+Round&display=swap'); 2 | 3 | /* - Variables - */ 4 | 5 | :root { 6 | --color-blue: #0AB5CD; 7 | --color-light-blue: #5ECEDB; 8 | 9 | --bg-color: #DEF2D9; 10 | --bg-dot-color: #FFF; 11 | 12 | --shadow-3: rgba(0, 0, 0, 0.03); 13 | --shadow-5: rgba(0, 0, 0, 0.05); 14 | --shadow-6: rgba(0, 0, 0, 0.06); 15 | --shadow-8: rgba(0, 0, 0, 0.08); 16 | --shadow-9: rgba(0, 0, 0, 0.09); 17 | --shadow-10: rgba(0, 0, 0, 0.10); 18 | --shadow-15: rgba(0, 0, 0, 0.16); 19 | --shadow-16: rgba(0, 0, 0, 0.16); 20 | --shadow-20: rgba(0, 0, 0, 0.20); 21 | 22 | --center-bg-color: #FFF; 23 | 24 | --wave-1: rgba(255, 255, 255, 0); 25 | --wave-2: rgba(255, 255, 255, 0.2); 26 | --wave-3: rgba(255, 255, 255, 0.4); 27 | --wave-4: rgba(255, 255, 255, 0.6); 28 | 29 | --nook-phone-bg-color: #F5F8FF; 30 | --nook-phone-text-color: #686868; 31 | 32 | --dialog-bg-color: #FFFAE5; 33 | --dialog-text-color: #837865; 34 | 35 | --dialog-name-bg-color: #FF9A40; 36 | --dialog-name-text-color: #BA3B1F; 37 | 38 | --chart-fill-color: var(--bg-color); 39 | --chart-line-color: rgba(0, 0, 0, 0.1); 40 | --chart-point-color: rgba(0, 0, 0, 0.1); 41 | 42 | --select-text-color: var(--dialog-text-color); 43 | --select-border-color: var(--bg-color); 44 | --select-bg-color-hover: #EBFEFD; 45 | 46 | --italic-color: #AAA; 47 | 48 | --form-h6-text-color: #845E44; 49 | 50 | --radio-hover-bg-color: var(--nook-phone-bg-color); 51 | --radio-checked-text-color: #FFF; 52 | 53 | --input-bg-color: #F3F3F3; 54 | --input-focus-bg-color: white; 55 | --input-focus-text-color: var(--color-blue); 56 | 57 | --input-now-bg-color: var(--dialog-name-bg-color); 58 | --input-now-text-color: var(--dialog-name-text-color); 59 | 60 | --button-text-color: var(--nook-phone-text-color); 61 | --button-reset-text-color: #E45B5B; 62 | 63 | --table-range0: hsl(140, 80%, 85%); 64 | --table-range1: hsl(90, 80%, 85%); 65 | --table-range2: hsl(60, 80%, 85%); 66 | --table-range3: hsl(30, 80%, 85%); 67 | --table-range4: hsl(0, 80%, 85%); 68 | } 69 | 70 | [data-theme="dark"] { 71 | --bg-color: #1A1A1A; 72 | --bg-dot-color: #222; 73 | 74 | --shadow-3: rgba(255, 255, 255, 0.03); 75 | --shadow-15: rgba(255, 255, 255, 0.03); 76 | 77 | --center-bg-color: #101010; 78 | 79 | --wave-1: rgba(16, 16, 16, 0); 80 | --wave-2: rgba(16, 16, 16, 0.2); 81 | --wave-3: rgba(16, 16, 16, 0.4); 82 | --wave-4: rgba(16, 16, 16, 0.6); 83 | 84 | --nook-phone-bg-color: #000F33; 85 | --nook-phone-text-color: #CCC; 86 | 87 | --dialog-bg-color: #252422; 88 | --dialog-text-color: #BCB5A9; 89 | 90 | --dialog-name-bg-color: #BA3B1F; 91 | --dialog-name-text-color: #FF9A40; 92 | 93 | --chart-fill-color: #2D5F21; 94 | --chart-line-color: rgba(200, 200, 200, 0.4); 95 | --chart-point-color: rgba(200, 200, 200, 0.6); 96 | 97 | --select-text-color: #837865; 98 | --select-border-color: var(--bg-color); 99 | --select-bg-color-hover: #EBFEFD; 100 | 101 | --italic-color: #666; 102 | 103 | --form-h6-text-color: #E18B51; 104 | 105 | --radio-hover-bg-color: #00174D; 106 | --radio-checked-text-color: #FFF; 107 | 108 | --input-bg-color: #333; 109 | --input-focus-bg-color: #999; 110 | --input-focus-text-color: var(--radio-hover-bg-color); 111 | 112 | --button-text-color: var(--nook-phone-text-color); 113 | --button-reset-text-color: #E45B5B; 114 | 115 | --table-range0: hsl(140, 80%, 27%); 116 | --table-range1: hsl(90, 80%, 20%); 117 | --table-range2: hsl(60, 80%, 20%); 118 | --table-range3: hsl(30, 80%, 20%); 119 | --table-range4: hsl(0, 80%, 22%); 120 | } 121 | 122 | /* - Global Styles - */ 123 | 124 | html { 125 | font-size: 14px; 126 | background: var(--bg-color); 127 | background-image: 128 | radial-gradient(var(--bg-dot-color) 20%, transparent 0), 129 | radial-gradient(var(--bg-dot-color) 20%, transparent 0); 130 | background-size: 30px 30px; 131 | background-position: 0 0, 15px 15px; 132 | } 133 | 134 | body { 135 | display: flex; 136 | flex-direction: column; 137 | align-items: center; 138 | justify-content: center; 139 | font-family: 'Varela Round', sans-serif; 140 | } 141 | 142 | h1 { 143 | text-align: center; 144 | font-size: 1.8rem; 145 | } 146 | 147 | h2 { 148 | text-align: center; 149 | font-size: 1.6rem; 150 | } 151 | 152 | .nook-phone { 153 | width: 100%; 154 | max-width: 1400px; 155 | box-sizing: border-box; 156 | margin: 16px auto; 157 | border-radius: 40px; 158 | padding: 16px 0px; 159 | padding-bottom: 16px; 160 | background: var(--nook-phone-bg-color); 161 | color: var(--nook-phone-text-color); 162 | overflow: hidden; 163 | box-shadow: 0 1px 3px var(--shadow-6), 0 1px 2px var(--shadow-8); 164 | } 165 | 166 | .nook-phone-center { 167 | background: var(--center-bg-color); 168 | display: flex; 169 | flex-direction: column; 170 | align-items: center; 171 | } 172 | 173 | .dialog-box { 174 | background: var(--dialog-bg-color); 175 | box-sizing: border-box; 176 | padding: 16px 24px; 177 | margin: 32px auto; 178 | position: relative; 179 | border-radius: 40px; 180 | max-width: 800px; 181 | box-shadow: 0 1px 3px var(--shadow-6), 0 1px 2px var(--shadow-8); 182 | } 183 | 184 | .dialog-box-option { 185 | text-align: center; 186 | } 187 | 188 | .dialog-box-option p, 189 | .dialog-box-option select { 190 | display: inline; 191 | } 192 | 193 | .dialog-box-option select { 194 | font-size: 1rem; 195 | padding: 4px; 196 | font-weight: bold; 197 | border-radius: 4px; 198 | border-color: var(--select-border-color); 199 | color: var(--select-text-color); 200 | cursor: pointer; 201 | transition: 0.2s all; 202 | } 203 | 204 | .dialog-box-option select:hover { 205 | background-color: var(--select-bg-color-hover); 206 | border-color: var(--color-light-blue); 207 | box-shadow: 0 2px 4px var(--shadow-16); 208 | } 209 | 210 | .dialog-box-option select:focus { 211 | outline: none; 212 | } 213 | 214 | .dialog-box p, 215 | .dialog-box label { 216 | font-family: 'Raleway', sans-serif; 217 | font-weight: 800; 218 | font-size: 1rem; 219 | color: var(--dialog-text-color); 220 | letter-spacing: 0.2px; 221 | line-height: 1.8rem; 222 | } 223 | 224 | .dialog-box b, 225 | .dialog-box a { 226 | color: var(--color-blue); 227 | transition: 0.2s all; 228 | } 229 | 230 | .dialog-box i { 231 | font-style: normal; 232 | color: var(--italic-color); 233 | } 234 | 235 | .dialog-box a:hover { 236 | color: var(--color-light-blue); 237 | } 238 | 239 | .dialog-box .dialog-box__name { 240 | position: absolute; 241 | left: 16px; 242 | top: -28px; 243 | font-size: 1rem; 244 | color: var(--dialog-name-text-color); 245 | padding: 4px 16px; 246 | background: var(--dialog-name-bg-color); 247 | border-radius: 40px; 248 | } 249 | 250 | .dialog-box.error { 251 | display: none; 252 | } 253 | 254 | .input__form { 255 | background: var(--center-bg-color); 256 | display: flex; 257 | flex-direction: column; 258 | padding: 16px; 259 | align-items: center; 260 | } 261 | 262 | .form__row { 263 | display: flex; 264 | flex-wrap: wrap; 265 | margin-bottom: 16px; 266 | justify-content: center; 267 | align-items: center; 268 | } 269 | 270 | .form__row h6 { 271 | width: 100%; 272 | display: block; 273 | font-weight: 800; 274 | font-size: 1.25rem; 275 | margin: 8px auto; 276 | color: var(--form-h6-text-color); 277 | text-align: center; 278 | } 279 | 280 | .form__flex-wrap { 281 | margin-top: 8px; 282 | display: flex; 283 | flex-wrap: wrap; 284 | width: 100%; 285 | max-width: 1080px; 286 | justify-content: center; 287 | } 288 | 289 | .input__group { 290 | display: flex; 291 | flex-direction: column; 292 | margin: 8px; 293 | align-items: center; 294 | } 295 | 296 | .input__group label { 297 | font-size: 1rem; 298 | font-weight: bold; 299 | margin-bottom: 8px; 300 | opacity: 0.7; 301 | text-align: center; 302 | } 303 | 304 | .form__flex-wrap .input__group label { 305 | margin-left: 0px; 306 | margin-bottom: 8px; 307 | } 308 | 309 | .input__form i { 310 | text-align: center; 311 | display: block; 312 | font-style: normal; 313 | color: var(--italic-color); 314 | font-size: 0.9rem; 315 | margin: 8px auto; 316 | } 317 | 318 | .input__form>.form__row input { 319 | margin: 0px auto; 320 | } 321 | 322 | input { 323 | border: 0px solid white; 324 | border-radius: 40px; 325 | padding: 8px 16px; 326 | font-size: 1.25rem; 327 | font-family: inherit; 328 | color: inherit; 329 | font-weight: bold; 330 | transition: 0.2s all; 331 | margin: 8px 0px; 332 | } 333 | 334 | input[type=number]:placeholder-shown { 335 | background: var(--input-bg-color); 336 | } 337 | 338 | input[type=number]:not(:placeholder-shown) { 339 | background: transparent; 340 | color: var(--color-blue); 341 | } 342 | 343 | input[type=number]:placeholder-shown:hover { 344 | cursor: pointer; 345 | background: var(--radio-hover-bg-color); 346 | transform: scale(1.1); 347 | box-shadow: 0 1px 6px var(--shadow-5), 0 3px 6px var(--shadow-9); 348 | } 349 | 350 | input[type=number]:focus { 351 | outline: none; 352 | transform: scale(1.1); 353 | color: var(--input-focus-text-color); 354 | background: var(--input-focus-bg-color); 355 | box-shadow: 0 1px 6px var(--shadow-5), 0 3px 6px var(--shadow-9); 356 | } 357 | 358 | input[type=number]:focus::placeholder { 359 | opacity: 0; 360 | } 361 | 362 | input[type=number] { 363 | width: 60px; 364 | text-align: center; 365 | } 366 | 367 | input[type=number]:disabled { 368 | background: inherit; 369 | } 370 | 371 | input[type=number]:disabled:hover { 372 | box-shadow: none; 373 | transform: none; 374 | cursor: default; 375 | } 376 | 377 | input::-webkit-outer-spin-button, 378 | input::-webkit-inner-spin-button { 379 | -webkit-appearance: none; 380 | margin: 0; 381 | } 382 | 383 | input[type=number] { 384 | -moz-appearance: textfield; 385 | } 386 | 387 | .input__radio-buttons { 388 | display: flex; 389 | flex-wrap: wrap; 390 | justify-content: center; 391 | margin-top: 8px; 392 | } 393 | 394 | .input__radio-buttons input[type=radio] { 395 | display: none; 396 | } 397 | 398 | .input__radio-buttons input[type="radio"]+label { 399 | opacity: 1; 400 | border: none; 401 | border-radius: 40px; 402 | background: var(--input-bg-color); 403 | padding: 8px 16px; 404 | font-size: 1.25rem; 405 | font-family: inherit; 406 | font-weight: bold; 407 | transition: 0.2s all; 408 | margin: 8px; 409 | } 410 | 411 | .input__radio-buttons input[type="radio"]:not(:checked)+label:hover { 412 | cursor: pointer; 413 | background: var(--radio-hover-bg-color); 414 | transform: scale(1.1); 415 | box-shadow: 0 1px 6px var(--shadow-5), 0 3px 6px var(--shadow-9); 416 | } 417 | 418 | .input__radio-buttons input[type="radio"]:checked+label { 419 | background: var(--color-blue); 420 | color: var(--radio-checked-text-color); 421 | } 422 | 423 | input[class=now]:placeholder-shown { 424 | background: var(--input-now-bg-color); 425 | } 426 | 427 | input[class=now]:placeholder-shown::placeholder { 428 | color: var(--input-now-text-color); 429 | } 430 | 431 | .button { 432 | color: var(--button-text-color); 433 | font-family: inherit; 434 | font-weight: bold; 435 | padding: 8px 16px; 436 | border-width: 0px; 437 | border-radius: 40px; 438 | background: var(--input-bg-color); 439 | font-size: 1.2rem; 440 | transition: 0.2s all; 441 | position: relative; 442 | margin: 16px auto; 443 | } 444 | 445 | .button:hover { 446 | transform: scale(1.1); 447 | cursor: pointer; 448 | background: var(--radio-hover-bg-color); 449 | opacity: 1; 450 | box-shadow: 0 1px 6px var(--shadow-5), 0 3px 6px var(--shadow-9); 451 | } 452 | 453 | .button.button--reset { 454 | color: var(--button-reset-text-color); 455 | } 456 | 457 | .table-wrapper { 458 | display: inline-block; 459 | max-width: 98%; 460 | padding: 16px; 461 | margin: 0px auto; 462 | box-sizing: border-box; 463 | overflow-x: auto; 464 | scrollbar-width: thin; 465 | } 466 | 467 | @media only screen and (max-width: 1440px) and (pointer: fine) { 468 | .table-wrapper { 469 | max-height: calc(75vh - 40px); 470 | } 471 | } 472 | 473 | .table-wrapper::-webkit-scrollbar { 474 | height: 8px; 475 | width: 5px; 476 | } 477 | 478 | .table-wrapper::-webkit-scrollbar-track { 479 | height: 8px; 480 | width: 5px; 481 | box-shadow: inset 0 0 6px var(--shadow-20); 482 | -webkit-box-shadow: inset 0 0 6px var(--shadow-20); 483 | } 484 | 485 | .table-wrapper::-webkit-scrollbar-thumb { 486 | height: 8px; 487 | width: 5px; 488 | background: var(--shadow-20); 489 | box-shadow: inset 0 0 6px var(--shadow-20); 490 | -webkit-box-shadow: inset 0 0 6px var(--shadow-10); 491 | } 492 | 493 | .table-wrapper::-webkit-scrollbar-thumb:window-inactive { 494 | height: 8px; 495 | width: 5px; 496 | background: var(--shadow-20); 497 | } 498 | 499 | #turnipTable { 500 | border-collapse: collapse; 501 | } 502 | 503 | #turnipTable th div:nth-of-type(1) { 504 | margin-bottom: 2px; 505 | } 506 | 507 | #turnipTable th div:nth-of-type(2) { 508 | display: flex; 509 | justify-content: space-around; 510 | opacity: 0.4; 511 | } 512 | 513 | #turnipTable td { 514 | white-space: nowrap; 515 | max-width: 150px; 516 | padding: 6px 4px; 517 | text-align: center; 518 | border-right: 1px solid var(--shadow-3); 519 | border-bottom: 1px solid var(--shadow-15); 520 | } 521 | 522 | #turnipTable tbody tr { 523 | opacity: 0.8; 524 | } 525 | 526 | #turnipTable tbody tr:hover { 527 | cursor: default; 528 | opacity: 1; 529 | } 530 | 531 | #turnipTable .table-pattern { 532 | white-space: nowrap; 533 | } 534 | 535 | #turnipTable td.range4 { 536 | background-color: var(--table-range4); 537 | } 538 | 539 | #turnipTable td.range3{ 540 | background-color: var(--table-range3); 541 | } 542 | 543 | #turnipTable td.range2 { 544 | background-color: var(--table-range2); 545 | } 546 | 547 | #turnipTable td.range1 { 548 | background-color: var(--table-range1); 549 | } 550 | 551 | #turnipTable td.range0 { 552 | background-color: var(--table-range0); 553 | } 554 | 555 | .chart-wrapper { 556 | margin-top: 8px; 557 | display: flex; 558 | flex-wrap: wrap; 559 | height: 400px; 560 | width: 100%; 561 | max-width: 1080px; 562 | justify-content: center; 563 | } 564 | 565 | .waves { 566 | position: relative; 567 | width: 100%; 568 | height: 5vh; 569 | margin-bottom: -7px; 570 | /*Fix for safari gap*/ 571 | max-height: 150px; 572 | } 573 | 574 | #permalink-input { 575 | display: none; 576 | position: fixed; 577 | } 578 | 579 | .permalink { 580 | display: none; 581 | white-space: nowrap; 582 | font-size: 18px; 583 | user-select: none; 584 | cursor: pointer; 585 | } 586 | 587 | .permalink .fa-copy { 588 | margin: 0 8px; 589 | height: 20px; 590 | color: var(--color-blue); 591 | } 592 | 593 | /* The snackbar - position it at the bottom and in the middle of the screen */ 594 | #snackbar { 595 | visibility: hidden; /* Hidden by default. Visible on click */ 596 | min-width: 250px; /* Set a default minimum width */ 597 | background-color: var(--dialog-bg-color); /* Black background color */ 598 | font-family: 'Raleway', sans-serif; 599 | font-weight: 800; 600 | font-size: 1rem; 601 | color: var(--dialog-text-color); 602 | letter-spacing: 0.2px; 603 | line-height: 1.8rem; 604 | text-align: center; /* Centered text */ 605 | border-radius: 40px; /* Rounded borders */ 606 | padding: 16px 24px; /* Padding */ 607 | position: fixed; /* Sit on top of the screen */ 608 | z-index: 1; /* Add a z-index if needed */ 609 | bottom: 30px; /* 30px from the bottom */ 610 | box-shadow: 0 1px 3px var(--shadow-6), 0 1px 2px var(--shadow-8); 611 | } 612 | 613 | /* Show the snackbar when clicking on a button (class added with JavaScript) */ 614 | #snackbar.show { 615 | visibility: visible; /* Show the snackbar */ 616 | /* Add animation: Take 0.5 seconds to fade in and out the snackbar. 617 | However, delay the fade out process for 2.5 seconds */ 618 | -webkit-animation: fadein 0.5s, fadeout 0.5s 2.5s; 619 | animation: fadein 0.5s, fadeout 0.5s 2.5s; 620 | } 621 | 622 | /* Animations to fade the snackbar in and out */ 623 | @-webkit-keyframes fadein { 624 | from {bottom: 0; opacity: 0;} 625 | to {bottom: 30px; opacity: 1;} 626 | } 627 | 628 | @keyframes fadein { 629 | from {bottom: 0; opacity: 0;} 630 | to {bottom: 30px; opacity: 1;} 631 | } 632 | 633 | @-webkit-keyframes fadeout { 634 | from {bottom: 30px; opacity: 1;} 635 | to {bottom: 0; opacity: 0;} 636 | } 637 | 638 | @keyframes fadeout { 639 | from {bottom: 30px; opacity: 1;} 640 | to {bottom: 0; opacity: 0;} 641 | } 642 | 643 | /* Cloud SVG placement */ 644 | .parallax>use:nth-child(1) { 645 | transform: translate3d(-30px, 0, 0); 646 | fill: var(--wave-4); 647 | } 648 | 649 | .parallax>use:nth-child(2) { 650 | transform: translate3d(-90px, 0, 0); 651 | fill: var(--wave-3); 652 | } 653 | 654 | .parallax>use:nth-child(3) { 655 | transform: translate3d(45px, 0, 0); 656 | fill: var(--wave-2); 657 | } 658 | 659 | .parallax>use:nth-child(4) { 660 | transform: translate3d(20px, 0, 0); 661 | fill: var(--wave-1); 662 | } 663 | 664 | /*Shrinking for mobile*/ 665 | @media (max-width: 768px) { 666 | .waves { 667 | height: 40px; 668 | min-height: 40px; 669 | } 670 | } 671 | -------------------------------------------------------------------------------- /js/predictions.js: -------------------------------------------------------------------------------- 1 | const PATTERN = { 2 | FLUCTUATING: 0, 3 | LARGE_SPIKE: 1, 4 | DECREASING: 2, 5 | SMALL_SPIKE: 3, 6 | }; 7 | 8 | const PROBABILITY_MATRIX = { 9 | [PATTERN.FLUCTUATING]: { 10 | [PATTERN.FLUCTUATING]: 0.20, 11 | [PATTERN.LARGE_SPIKE]: 0.30, 12 | [PATTERN.DECREASING]: 0.15, 13 | [PATTERN.SMALL_SPIKE]: 0.35, 14 | }, 15 | [PATTERN.LARGE_SPIKE]: { 16 | [PATTERN.FLUCTUATING]: 0.50, 17 | [PATTERN.LARGE_SPIKE]: 0.05, 18 | [PATTERN.DECREASING]: 0.20, 19 | [PATTERN.SMALL_SPIKE]: 0.25, 20 | }, 21 | [PATTERN.DECREASING]: { 22 | [PATTERN.FLUCTUATING]: 0.25, 23 | [PATTERN.LARGE_SPIKE]: 0.45, 24 | [PATTERN.DECREASING]: 0.05, 25 | [PATTERN.SMALL_SPIKE]: 0.25, 26 | }, 27 | [PATTERN.SMALL_SPIKE]: { 28 | [PATTERN.FLUCTUATING]: 0.45, 29 | [PATTERN.LARGE_SPIKE]: 0.25, 30 | [PATTERN.DECREASING]: 0.15, 31 | [PATTERN.SMALL_SPIKE]: 0.15, 32 | }, 33 | }; 34 | 35 | const RATE_MULTIPLIER = 10000; 36 | 37 | function range_length(range) { 38 | return range[1] - range[0]; 39 | } 40 | 41 | function clamp(x, min, max) { 42 | return Math.min(Math.max(x, min), max); 43 | } 44 | 45 | function range_intersect(range1, range2) { 46 | if (range1[0] > range2[1] || range1[1] < range2[0]) { 47 | return null; 48 | } 49 | return [Math.max(range1[0], range2[0]), Math.min(range1[1], range2[1])]; 50 | } 51 | 52 | function range_intersect_length(range1, range2) { 53 | if (range1[0] > range2[1] || range1[1] < range2[0]) { 54 | return 0; 55 | } 56 | return range_length(range_intersect(range1, range2)); 57 | } 58 | 59 | /** 60 | * Accurately sums a list of floating point numbers. 61 | * See https://en.wikipedia.org/wiki/Kahan_summation_algorithm#Further_enhancements 62 | * for more information. 63 | * @param {number[]} input 64 | * @returns {number} The sum of the input. 65 | */ 66 | function float_sum(input) { 67 | // Uses the improved Kahan–Babuska algorithm introduced by Neumaier. 68 | let sum = 0; 69 | // The "lost bits" of sum. 70 | let c = 0; 71 | for (let i = 0; i < input.length; i++) { 72 | const cur = input[i]; 73 | const t = sum + cur; 74 | if (Math.abs(sum) >= Math.abs(cur)) { 75 | c += (sum - t) + cur; 76 | } else { 77 | c += (cur - t) + sum; 78 | } 79 | sum = t; 80 | } 81 | return sum + c; 82 | } 83 | 84 | /** 85 | * Accurately returns the prefix sum of a list of floating point numbers. 86 | * See https://en.wikipedia.org/wiki/Kahan_summation_algorithm#Further_enhancements 87 | * for more information. 88 | * @param {number[]} input 89 | * @returns {[number, number][]} The prefix sum of the input, such that 90 | * output[i] = [sum of first i integers, error of the sum]. 91 | * The "true" prefix sum is equal to the sum of the pair of numbers, but it is 92 | * explicitly returned as a pair of numbers to ensure that the error portion 93 | * isn't lost when subtracting prefix sums. 94 | */ 95 | function prefix_float_sum(input) { 96 | const prefix_sum = [[0, 0]]; 97 | let sum = 0; 98 | let c = 0; 99 | for (let i = 0; i < input.length; i++) { 100 | const cur = input[i]; 101 | const t = sum + cur; 102 | if (Math.abs(sum) >= Math.abs(cur)) { 103 | c += (sum - t) + cur; 104 | } else { 105 | c += (cur - t) + sum; 106 | } 107 | sum = t; 108 | prefix_sum.push([sum, c]); 109 | } 110 | return prefix_sum; 111 | } 112 | 113 | /* 114 | * Probability Density Function of rates. 115 | * Since the PDF is continuous*, we approximate it by a discrete probability function: 116 | * the value in range [x, x + 1) has a uniform probability 117 | * prob[x - value_start]; 118 | * 119 | * Note that we operate all rate on the (* RATE_MULTIPLIER) scale. 120 | * 121 | * (*): Well not really since it only takes values that "float" can represent in some form, but the 122 | * space is too large to compute directly in JS. 123 | */ 124 | class PDF { 125 | /** 126 | * Initialize a PDF in range [a, b], a and b can be non-integer. 127 | * if uniform is true, then initialize the probability to be uniform, else initialize to a 128 | * all-zero (invalid) PDF. 129 | * @param {number} a - Left end-point. 130 | * @param {number} b - Right end-point end-point. 131 | * @param {boolean} uniform - If true, initialise with the uniform distribution. 132 | */ 133 | constructor(a, b, uniform = true) { 134 | // We need to ensure that [a, b] is fully contained in [value_start, value_end]. 135 | /** @type {number} */ 136 | this.value_start = Math.floor(a); 137 | /** @type {number} */ 138 | this.value_end = Math.ceil(b); 139 | const range = [a, b]; 140 | const total_length = range_length(range); 141 | /** @type {number[]} */ 142 | this.prob = Array(this.value_end - this.value_start); 143 | if (uniform) { 144 | for (let i = 0; i < this.prob.length; i++) { 145 | this.prob[i] = 146 | range_intersect_length(this.range_of(i), range) / total_length; 147 | } 148 | } 149 | } 150 | 151 | /** 152 | * Calculates the interval represented by this.prob[idx] 153 | * @param {number} idx - The index of this.prob 154 | * @returns {[number, number]} The interval representing this.prob[idx]. 155 | */ 156 | range_of(idx) { 157 | // We intentionally include the right end-point of the range. 158 | // The probability of getting exactly an endpoint is zero, so we can assume 159 | // the "probability ranges" are "touching". 160 | return [this.value_start + idx, this.value_start + idx + 1]; 161 | } 162 | 163 | min_value() { 164 | return this.value_start; 165 | } 166 | 167 | max_value() { 168 | return this.value_end; 169 | } 170 | 171 | /** 172 | * @returns {number} The sum of probabilities before normalisation. 173 | */ 174 | normalize() { 175 | const total_probability = float_sum(this.prob); 176 | for (let i = 0; i < this.prob.length; i++) { 177 | this.prob[i] /= total_probability; 178 | } 179 | return total_probability; 180 | } 181 | 182 | /* 183 | * Limit the values to be in the range, and return the probability that the value was in this 184 | * range. 185 | */ 186 | range_limit(range) { 187 | let [start, end] = range; 188 | start = Math.max(start, this.min_value()); 189 | end = Math.min(end, this.max_value()); 190 | if (start >= end) { 191 | // Set this to invalid values 192 | this.value_start = this.value_end = 0; 193 | this.prob = []; 194 | return 0; 195 | } 196 | start = Math.floor(start); 197 | end = Math.ceil(end); 198 | 199 | const start_idx = start - this.value_start; 200 | const end_idx = end - this.value_start; 201 | for (let i = start_idx; i < end_idx; i++) { 202 | this.prob[i] *= range_intersect_length(this.range_of(i), range); 203 | } 204 | 205 | this.prob = this.prob.slice(start_idx, end_idx); 206 | this.value_start = start; 207 | this.value_end = end; 208 | 209 | // The probability that the value was in this range is equal to the total 210 | // sum of "un-normalised" values in the range. 211 | return this.normalize(); 212 | } 213 | 214 | /** 215 | * Subtract the PDF by a uniform distribution in [rate_decay_min, rate_decay_max] 216 | * 217 | * For simplicity, we assume that rate_decay_min and rate_decay_max are both integers. 218 | * @param {number} rate_decay_min 219 | * @param {number} rate_decay_max 220 | * @returns {void} 221 | */ 222 | decay(rate_decay_min, rate_decay_max) { 223 | // In case the arguments aren't integers, round them to the nearest integer. 224 | rate_decay_min = Math.round(rate_decay_min); 225 | rate_decay_max = Math.round(rate_decay_max); 226 | // The sum of this distribution with a uniform distribution. 227 | // Let's assume that both distributions start at 0 and X = this dist, 228 | // Y = uniform dist, and Z = X + Y. 229 | // Let's also assume that X is a "piecewise uniform" distribution, so 230 | // x(i) = this.prob[Math.floor(i)] - which matches our implementation. 231 | // We also know that y(i) = 1 / max(Y) - as we assume that min(Y) = 0. 232 | // In the end, we're interested in: 233 | // Pr(i <= Z < i+1) where i is an integer 234 | // = int. x(val) * Pr(i-val <= Y < i-val+1) dval from 0 to max(X) 235 | // = int. x(floor(val)) * Pr(i-val <= Y < i-val+1) dval from 0 to max(X) 236 | // = sum val from 0 to max(X)-1 237 | // x(val) * f_i(val) / max(Y) 238 | // where f_i(val) = 239 | // 0.5 if i-val = 0 or max(Y), so val = i-max(Y) or i 240 | // 1.0 if 0 < i-val < max(Y), so i-max(Y) < val < i 241 | // as x(val) is "constant" for each integer step, so we can consider the 242 | // integral in integer steps. 243 | // = sum val from max(0, i-max(Y)) to min(max(X)-1, i) 244 | // x(val) * f_i(val) / max(Y) 245 | // for example, max(X)=1, max(Y)=10, i=5 246 | // = sum val from max(0, 5-10)=0 to min(1-1, 5)=0 247 | // x(val) * f_i(val) / max(Y) 248 | // = x(0) * 1 / 10 249 | 250 | // Get a prefix sum / CDF of this so we can calculate sums in O(1). 251 | const prefix = prefix_float_sum(this.prob); 252 | const max_X = this.prob.length; 253 | const max_Y = rate_decay_max - rate_decay_min; 254 | const newProb = Array(this.prob.length + max_Y); 255 | for (let i = 0; i < newProb.length; i++) { 256 | // Note that left and right here are INCLUSIVE. 257 | const left = Math.max(0, i - max_Y); 258 | const right = Math.min(max_X - 1, i); 259 | // We want to sum, in total, prefix[right+1], -prefix[left], and subtract 260 | // the 0.5s if necessary. 261 | // This may involve numbers of differing magnitudes, so use the float sum 262 | // algorithm to sum these up. 263 | const numbers_to_sum = [ 264 | prefix[right + 1][0], prefix[right + 1][1], 265 | -prefix[left][0], -prefix[left][1], 266 | ]; 267 | if (left === i-max_Y) { 268 | // Need to halve the left endpoint. 269 | numbers_to_sum.push(-this.prob[left] / 2); 270 | } 271 | if (right === i) { 272 | // Need to halve the right endpoint. 273 | // It's guaranteed that we won't accidentally "halve" twice, 274 | // as that would require i-max_Y = i, so max_Y = 0 - which is 275 | // impossible. 276 | numbers_to_sum.push(-this.prob[right] / 2); 277 | } 278 | newProb[i] = float_sum(numbers_to_sum) / max_Y; 279 | } 280 | 281 | this.prob = newProb; 282 | this.value_start -= rate_decay_max; 283 | this.value_end -= rate_decay_min; 284 | // No need to normalise, as it is guaranteed that the sum of this.prob is 1. 285 | } 286 | } 287 | 288 | class Predictor { 289 | 290 | constructor(prices, first_buy, previous_pattern) { 291 | // The reverse-engineered code is not perfectly accurate, especially as it's not 292 | // 32-bit ARM floating point. So, be tolerant of slightly unexpected inputs 293 | this.fudge_factor = 0; 294 | this.prices = prices; 295 | this.first_buy = first_buy; 296 | this.previous_pattern = previous_pattern; 297 | } 298 | 299 | intceil(val) { 300 | return Math.trunc(val + 0.99999); 301 | } 302 | 303 | minimum_rate_from_given_and_base(given_price, buy_price) { 304 | return RATE_MULTIPLIER * (given_price - 0.99999) / buy_price; 305 | } 306 | 307 | maximum_rate_from_given_and_base(given_price, buy_price) { 308 | return RATE_MULTIPLIER * (given_price + 0.00001) / buy_price; 309 | } 310 | 311 | rate_range_from_given_and_base(given_price, buy_price) { 312 | return [ 313 | this.minimum_rate_from_given_and_base(given_price, buy_price), 314 | this.maximum_rate_from_given_and_base(given_price, buy_price) 315 | ]; 316 | } 317 | 318 | get_price(rate, basePrice) { 319 | return this.intceil(rate * basePrice / RATE_MULTIPLIER); 320 | } 321 | 322 | * multiply_generator_probability(generator, probability) { 323 | for (const it of generator) { 324 | yield {...it, probability: it.probability * probability}; 325 | } 326 | } 327 | 328 | /* 329 | * This corresponds to the code: 330 | * for (int i = start; i < start + length; i++) 331 | * { 332 | * sellPrices[work++] = 333 | * intceil(randfloat(rate_min / RATE_MULTIPLIER, rate_max / RATE_MULTIPLIER) * basePrice); 334 | * } 335 | * 336 | * Would return the conditional probability given the given_prices, and modify 337 | * the predicted_prices array. 338 | * If the given_prices won't match, returns 0. 339 | */ 340 | generate_individual_random_price( 341 | given_prices, predicted_prices, start, length, rate_min, rate_max) { 342 | rate_min *= RATE_MULTIPLIER; 343 | rate_max *= RATE_MULTIPLIER; 344 | 345 | const buy_price = given_prices[0]; 346 | const rate_range = [rate_min, rate_max]; 347 | let prob = 1; 348 | 349 | for (let i = start; i < start + length; i++) { 350 | let min_pred = this.get_price(rate_min, buy_price); 351 | let max_pred = this.get_price(rate_max, buy_price); 352 | if (!isNaN(given_prices[i])) { 353 | if (given_prices[i] < min_pred - this.fudge_factor || given_prices[i] > max_pred + this.fudge_factor) { 354 | // Given price is out of predicted range, so this is the wrong pattern 355 | return 0; 356 | } 357 | // TODO: How to deal with probability when there's fudge factor? 358 | // Clamp the value to be in range now so the probability won't be totally biased to fudged values. 359 | const real_rate_range = 360 | this.rate_range_from_given_and_base(clamp(given_prices[i], min_pred, max_pred), buy_price); 361 | prob *= range_intersect_length(rate_range, real_rate_range) / 362 | range_length(rate_range); 363 | min_pred = given_prices[i]; 364 | max_pred = given_prices[i]; 365 | } 366 | 367 | predicted_prices.push({ 368 | min: min_pred, 369 | max: max_pred, 370 | }); 371 | } 372 | return prob; 373 | } 374 | 375 | /* 376 | * This corresponds to the code: 377 | * rate = randfloat(start_rate_min, start_rate_max); 378 | * for (int i = start; i < start + length; i++) 379 | * { 380 | * sellPrices[work++] = intceil(rate * basePrice); 381 | * rate -= randfloat(rate_decay_min, rate_decay_max); 382 | * } 383 | * 384 | * Would return the conditional probability given the given_prices, and modify 385 | * the predicted_prices array. 386 | * If the given_prices won't match, returns 0. 387 | */ 388 | generate_decreasing_random_price( 389 | given_prices, predicted_prices, start, length, start_rate_min, 390 | start_rate_max, rate_decay_min, rate_decay_max) { 391 | start_rate_min *= RATE_MULTIPLIER; 392 | start_rate_max *= RATE_MULTIPLIER; 393 | rate_decay_min *= RATE_MULTIPLIER; 394 | rate_decay_max *= RATE_MULTIPLIER; 395 | 396 | const buy_price = given_prices[0]; 397 | let rate_pdf = new PDF(start_rate_min, start_rate_max); 398 | let prob = 1; 399 | 400 | for (let i = start; i < start + length; i++) { 401 | let min_pred = this.get_price(rate_pdf.min_value(), buy_price); 402 | let max_pred = this.get_price(rate_pdf.max_value(), buy_price); 403 | if (!isNaN(given_prices[i])) { 404 | if (given_prices[i] < min_pred - this.fudge_factor || given_prices[i] > max_pred + this.fudge_factor) { 405 | // Given price is out of predicted range, so this is the wrong pattern 406 | return 0; 407 | } 408 | // TODO: How to deal with probability when there's fudge factor? 409 | // Clamp the value to be in range now so the probability won't be totally biased to fudged values. 410 | const real_rate_range = 411 | this.rate_range_from_given_and_base(clamp(given_prices[i], min_pred, max_pred), buy_price); 412 | prob *= rate_pdf.range_limit(real_rate_range); 413 | if (prob == 0) { 414 | return 0; 415 | } 416 | min_pred = given_prices[i]; 417 | max_pred = given_prices[i]; 418 | } 419 | 420 | predicted_prices.push({ 421 | min: min_pred, 422 | max: max_pred, 423 | }); 424 | 425 | rate_pdf.decay(rate_decay_min, rate_decay_max); 426 | } 427 | return prob; 428 | } 429 | 430 | 431 | /* 432 | * This corresponds to the code: 433 | * rate = randfloat(rate_min, rate_max); 434 | * sellPrices[work++] = intceil(randfloat(rate_min, rate) * basePrice) - 1; 435 | * sellPrices[work++] = intceil(rate * basePrice); 436 | * sellPrices[work++] = intceil(randfloat(rate_min, rate) * basePrice) - 1; 437 | * 438 | * Would return the conditional probability given the given_prices, and modify 439 | * the predicted_prices array. 440 | * If the given_prices won't match, returns 0. 441 | */ 442 | generate_peak_price( 443 | given_prices, predicted_prices, start, rate_min, rate_max) { 444 | rate_min *= RATE_MULTIPLIER; 445 | rate_max *= RATE_MULTIPLIER; 446 | 447 | const buy_price = given_prices[0]; 448 | let prob = 1; 449 | let rate_range = [rate_min, rate_max]; 450 | 451 | // * Calculate the probability first. 452 | // Prob(middle_price) 453 | const middle_price = given_prices[start + 1]; 454 | if (!isNaN(middle_price)) { 455 | const min_pred = this.get_price(rate_min, buy_price); 456 | const max_pred = this.get_price(rate_max, buy_price); 457 | if (middle_price < min_pred - this.fudge_factor || middle_price > max_pred + this.fudge_factor) { 458 | // Given price is out of predicted range, so this is the wrong pattern 459 | return 0; 460 | } 461 | // TODO: How to deal with probability when there's fudge factor? 462 | // Clamp the value to be in range now so the probability won't be totally biased to fudged values. 463 | const real_rate_range = 464 | this.rate_range_from_given_and_base(clamp(middle_price, min_pred, max_pred), buy_price); 465 | prob *= range_intersect_length(rate_range, real_rate_range) / 466 | range_length(rate_range); 467 | if (prob == 0) { 468 | return 0; 469 | } 470 | 471 | rate_range = range_intersect(rate_range, real_rate_range); 472 | } 473 | 474 | const left_price = given_prices[start]; 475 | const right_price = given_prices[start + 2]; 476 | // Prob(left_price | middle_price), Prob(right_price | middle_price) 477 | // 478 | // A = rate_range[0], B = rate_range[1], C = rate_min, X = rate, Y = randfloat(rate_min, rate) 479 | // rate = randfloat(A, B); sellPrices[work++] = intceil(randfloat(C, rate) * basePrice) - 1; 480 | // 481 | // => X->U(A,B), Y->U(C,X), Y-C->U(0,X-C), Y-C->U(0,1)*(X-C), Y-C->U(0,1)*U(A-C,B-C), 482 | // let Z=Y-C, Z1=A-C, Z2=B-C, Z->U(0,1)*U(Z1,Z2) 483 | // Prob(Z<=t) = integral_{x=0}^{1} [min(t/x,Z2)-min(t/x,Z1)]/ (Z2-Z1) 484 | // let F(t, ZZ) = integral_{x=0}^{1} min(t/x, ZZ) 485 | // 1. if ZZ < t, then min(t/x, ZZ) = ZZ -> F(t, ZZ) = ZZ 486 | // 2. if ZZ >= t, then F(t, ZZ) = integral_{x=0}^{t/ZZ} ZZ + integral_{x=t/ZZ}^{1} t/x 487 | // = t - t log(t/ZZ) 488 | // Prob(Z<=t) = (F(t, Z2) - F(t, Z1)) / (Z2 - Z1) 489 | // Prob(Y<=t) = Prob(Z>=t-C) 490 | for (const price of [left_price, right_price]) { 491 | if (isNaN(price)) { 492 | continue; 493 | } 494 | const min_pred = this.get_price(rate_min, buy_price) - 1; 495 | const max_pred = this.get_price(rate_range[1], buy_price) - 1; 496 | if (price < min_pred - this.fudge_factor || price > max_pred + this.fudge_factor) { 497 | // Given price is out of predicted range, so this is the wrong pattern 498 | return 0; 499 | } 500 | // TODO: How to deal with probability when there's fudge factor? 501 | // Clamp the value to be in range now so the probability won't be totally biased to fudged values. 502 | const rate2_range = this.rate_range_from_given_and_base(clamp(price, min_pred, max_pred)+ 1, buy_price); 503 | const F = (t, ZZ) => { 504 | if (t <= 0) { 505 | return 0; 506 | } 507 | return ZZ < t ? ZZ : t - t * (Math.log(t) - Math.log(ZZ)); 508 | }; 509 | const [A, B] = rate_range; 510 | const C = rate_min; 511 | const Z1 = A - C; 512 | const Z2 = B - C; 513 | const PY = (t) => (F(t - C, Z2) - F(t - C, Z1)) / (Z2 - Z1); 514 | prob *= PY(rate2_range[1]) - PY(rate2_range[0]); 515 | if (prob == 0) { 516 | return 0; 517 | } 518 | } 519 | 520 | // * Then generate the real predicted range. 521 | // We're doing things in different order then how we calculate probability, 522 | // since forward prediction is more useful here. 523 | // 524 | // Main spike 1 525 | let min_pred = this.get_price(rate_min, buy_price) - 1; 526 | let max_pred = this.get_price(rate_max, buy_price) - 1; 527 | if (!isNaN(given_prices[start])) { 528 | min_pred = given_prices[start]; 529 | max_pred = given_prices[start]; 530 | } 531 | predicted_prices.push({ 532 | min: min_pred, 533 | max: max_pred, 534 | }); 535 | 536 | // Main spike 2 537 | min_pred = predicted_prices[start].min; 538 | max_pred = this.get_price(rate_max, buy_price); 539 | if (!isNaN(given_prices[start + 1])) { 540 | min_pred = given_prices[start + 1]; 541 | max_pred = given_prices[start + 1]; 542 | } 543 | predicted_prices.push({ 544 | min: min_pred, 545 | max: max_pred, 546 | }); 547 | 548 | // Main spike 3 549 | min_pred = this.get_price(rate_min, buy_price) - 1; 550 | max_pred = predicted_prices[start + 1].max - 1; 551 | if (!isNaN(given_prices[start + 2])) { 552 | min_pred = given_prices[start + 2]; 553 | max_pred = given_prices[start + 2]; 554 | } 555 | predicted_prices.push({ 556 | min: min_pred, 557 | max: max_pred, 558 | }); 559 | 560 | return prob; 561 | } 562 | 563 | * generate_pattern_0_with_lengths( 564 | given_prices, high_phase_1_len, dec_phase_1_len, high_phase_2_len, 565 | dec_phase_2_len, high_phase_3_len) { 566 | /* 567 | // PATTERN 0: high, decreasing, high, decreasing, high 568 | work = 2; 569 | // high phase 1 570 | for (int i = 0; i < hiPhaseLen1; i++) 571 | { 572 | sellPrices[work++] = intceil(randfloat(0.9, 1.4) * basePrice); 573 | } 574 | // decreasing phase 1 575 | rate = randfloat(0.8, 0.6); 576 | for (int i = 0; i < decPhaseLen1; i++) 577 | { 578 | sellPrices[work++] = intceil(rate * basePrice); 579 | rate -= 0.04; 580 | rate -= randfloat(0, 0.06); 581 | } 582 | // high phase 2 583 | for (int i = 0; i < (hiPhaseLen2and3 - hiPhaseLen3); i++) 584 | { 585 | sellPrices[work++] = intceil(randfloat(0.9, 1.4) * basePrice); 586 | } 587 | // decreasing phase 2 588 | rate = randfloat(0.8, 0.6); 589 | for (int i = 0; i < decPhaseLen2; i++) 590 | { 591 | sellPrices[work++] = intceil(rate * basePrice); 592 | rate -= 0.04; 593 | rate -= randfloat(0, 0.06); 594 | } 595 | // high phase 3 596 | for (int i = 0; i < hiPhaseLen3; i++) 597 | { 598 | sellPrices[work++] = intceil(randfloat(0.9, 1.4) * basePrice); 599 | } 600 | */ 601 | 602 | const buy_price = given_prices[0]; 603 | const predicted_prices = [ 604 | { 605 | min: buy_price, 606 | max: buy_price, 607 | }, 608 | { 609 | min: buy_price, 610 | max: buy_price, 611 | }, 612 | ]; 613 | let probability = 1; 614 | 615 | // High Phase 1 616 | probability *= this.generate_individual_random_price( 617 | given_prices, predicted_prices, 2, high_phase_1_len, 0.9, 1.4); 618 | if (probability == 0) { 619 | return; 620 | } 621 | 622 | // Dec Phase 1 623 | probability *= this.generate_decreasing_random_price( 624 | given_prices, predicted_prices, 2 + high_phase_1_len, dec_phase_1_len, 625 | 0.6, 0.8, 0.04, 0.1); 626 | if (probability == 0) { 627 | return; 628 | } 629 | 630 | // High Phase 2 631 | probability *= this.generate_individual_random_price(given_prices, predicted_prices, 632 | 2 + high_phase_1_len + dec_phase_1_len, high_phase_2_len, 0.9, 1.4); 633 | if (probability == 0) { 634 | return; 635 | } 636 | 637 | // Dec Phase 2 638 | probability *= this.generate_decreasing_random_price( 639 | given_prices, predicted_prices, 640 | 2 + high_phase_1_len + dec_phase_1_len + high_phase_2_len, 641 | dec_phase_2_len, 0.6, 0.8, 0.04, 0.1); 642 | if (probability == 0) { 643 | return; 644 | } 645 | 646 | // High Phase 3 647 | if (2 + high_phase_1_len + dec_phase_1_len + high_phase_2_len + dec_phase_2_len + high_phase_3_len != 14) { 648 | throw new Error("Phase lengths don't add up"); 649 | } 650 | 651 | const prev_length = 2 + high_phase_1_len + dec_phase_1_len + 652 | high_phase_2_len + dec_phase_2_len; 653 | probability *= this.generate_individual_random_price( 654 | given_prices, predicted_prices, prev_length, 14 - prev_length, 0.9, 1.4); 655 | if (probability == 0) { 656 | return; 657 | } 658 | 659 | yield { 660 | pattern_number: 0, 661 | prices: predicted_prices, 662 | probability, 663 | }; 664 | } 665 | 666 | * generate_pattern_0(given_prices) { 667 | /* 668 | decPhaseLen1 = randbool() ? 3 : 2; 669 | decPhaseLen2 = 5 - decPhaseLen1; 670 | hiPhaseLen1 = randint(0, 6); 671 | hiPhaseLen2and3 = 7 - hiPhaseLen1; 672 | hiPhaseLen3 = randint(0, hiPhaseLen2and3 - 1); 673 | */ 674 | for (var dec_phase_1_len = 2; dec_phase_1_len < 4; dec_phase_1_len++) { 675 | for (var high_phase_1_len = 0; high_phase_1_len < 7; high_phase_1_len++) { 676 | for (var high_phase_3_len = 0; high_phase_3_len < (7 - high_phase_1_len - 1 + 1); high_phase_3_len++) { 677 | yield* this.multiply_generator_probability( 678 | this.generate_pattern_0_with_lengths(given_prices, high_phase_1_len, dec_phase_1_len, 7 - high_phase_1_len - high_phase_3_len, 5 - dec_phase_1_len, high_phase_3_len), 679 | 1 / (4 - 2) / 7 / (7 - high_phase_1_len)); 680 | } 681 | } 682 | } 683 | } 684 | 685 | * generate_pattern_1_with_peak(given_prices, peak_start) { 686 | /* 687 | // PATTERN 1: decreasing middle, high spike, random low 688 | peakStart = randint(3, 9); 689 | rate = randfloat(0.9, 0.85); 690 | for (work = 2; work < peakStart; work++) 691 | { 692 | sellPrices[work] = intceil(rate * basePrice); 693 | rate -= 0.03; 694 | rate -= randfloat(0, 0.02); 695 | } 696 | sellPrices[work++] = intceil(randfloat(0.9, 1.4) * basePrice); 697 | sellPrices[work++] = intceil(randfloat(1.4, 2.0) * basePrice); 698 | sellPrices[work++] = intceil(randfloat(2.0, 6.0) * basePrice); 699 | sellPrices[work++] = intceil(randfloat(1.4, 2.0) * basePrice); 700 | sellPrices[work++] = intceil(randfloat(0.9, 1.4) * basePrice); 701 | for (; work < 14; work++) 702 | { 703 | sellPrices[work] = intceil(randfloat(0.4, 0.9) * basePrice); 704 | } 705 | */ 706 | 707 | const buy_price = given_prices[0]; 708 | const predicted_prices = [ 709 | { 710 | min: buy_price, 711 | max: buy_price, 712 | }, 713 | { 714 | min: buy_price, 715 | max: buy_price, 716 | }, 717 | ]; 718 | let probability = 1; 719 | 720 | probability *= this.generate_decreasing_random_price( 721 | given_prices, predicted_prices, 2, peak_start - 2, 0.85, 0.9, 0.03, 0.05); 722 | if (probability == 0) { 723 | return; 724 | } 725 | 726 | // Now each day is independent of next 727 | let min_randoms = [0.9, 1.4, 2.0, 1.4, 0.9, 0.4, 0.4, 0.4, 0.4, 0.4, 0.4]; 728 | let max_randoms = [1.4, 2.0, 6.0, 2.0, 1.4, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9]; 729 | for (let i = peak_start; i < 14; i++) { 730 | probability *= this.generate_individual_random_price( 731 | given_prices, predicted_prices, i, 1, min_randoms[i - peak_start], 732 | max_randoms[i - peak_start]); 733 | if (probability == 0) { 734 | return; 735 | } 736 | } 737 | yield { 738 | pattern_number: 1, 739 | prices: predicted_prices, 740 | probability, 741 | }; 742 | } 743 | 744 | * generate_pattern_1(given_prices) { 745 | for (var peak_start = 3; peak_start < 10; peak_start++) { 746 | yield* this.multiply_generator_probability(this.generate_pattern_1_with_peak(given_prices, peak_start), 1 / (10 - 3)); 747 | } 748 | } 749 | 750 | * generate_pattern_2(given_prices) { 751 | /* 752 | // PATTERN 2: consistently decreasing 753 | rate = 0.9; 754 | rate -= randfloat(0, 0.05); 755 | for (work = 2; work < 14; work++) 756 | { 757 | sellPrices[work] = intceil(rate * basePrice); 758 | rate -= 0.03; 759 | rate -= randfloat(0, 0.02); 760 | } 761 | break; 762 | */ 763 | 764 | const buy_price = given_prices[0]; 765 | const predicted_prices = [ 766 | { 767 | min: buy_price, 768 | max: buy_price, 769 | }, 770 | { 771 | min: buy_price, 772 | max: buy_price, 773 | }, 774 | ]; 775 | let probability = 1; 776 | 777 | probability *= this.generate_decreasing_random_price( 778 | given_prices, predicted_prices, 2, 14 - 2, 0.85, 0.9, 0.03, 0.05); 779 | if (probability == 0) { 780 | return; 781 | } 782 | 783 | yield { 784 | pattern_number: 2, 785 | prices: predicted_prices, 786 | probability, 787 | }; 788 | } 789 | 790 | * generate_pattern_3_with_peak(given_prices, peak_start) { 791 | 792 | /* 793 | // PATTERN 3: decreasing, spike, decreasing 794 | peakStart = randint(2, 9); 795 | // decreasing phase before the peak 796 | rate = randfloat(0.9, 0.4); 797 | for (work = 2; work < peakStart; work++) 798 | { 799 | sellPrices[work] = intceil(rate * basePrice); 800 | rate -= 0.03; 801 | rate -= randfloat(0, 0.02); 802 | } 803 | sellPrices[work++] = intceil(randfloat(0.9, 1.4) * (float)basePrice); 804 | sellPrices[work++] = intceil(randfloat(0.9, 1.4) * basePrice); 805 | rate = randfloat(1.4, 2.0); 806 | sellPrices[work++] = intceil(randfloat(1.4, rate) * basePrice) - 1; 807 | sellPrices[work++] = intceil(rate * basePrice); 808 | sellPrices[work++] = intceil(randfloat(1.4, rate) * basePrice) - 1; 809 | // decreasing phase after the peak 810 | if (work < 14) 811 | { 812 | rate = randfloat(0.9, 0.4); 813 | for (; work < 14; work++) 814 | { 815 | sellPrices[work] = intceil(rate * basePrice); 816 | rate -= 0.03; 817 | rate -= randfloat(0, 0.02); 818 | } 819 | } 820 | */ 821 | 822 | const buy_price = given_prices[0]; 823 | const predicted_prices = [ 824 | { 825 | min: buy_price, 826 | max: buy_price, 827 | }, 828 | { 829 | min: buy_price, 830 | max: buy_price, 831 | }, 832 | ]; 833 | let probability = 1; 834 | 835 | probability *= this.generate_decreasing_random_price( 836 | given_prices, predicted_prices, 2, peak_start - 2, 0.4, 0.9, 0.03, 0.05); 837 | if (probability == 0) { 838 | return; 839 | } 840 | 841 | // The peak 842 | probability *= this.generate_individual_random_price( 843 | given_prices, predicted_prices, peak_start, 2, 0.9, 1.4); 844 | if (probability == 0) { 845 | return; 846 | } 847 | 848 | probability *= this.generate_peak_price( 849 | given_prices, predicted_prices, peak_start + 2, 1.4, 2.0); 850 | if (probability == 0) { 851 | return; 852 | } 853 | 854 | if (peak_start + 5 < 14) { 855 | probability *= this.generate_decreasing_random_price( 856 | given_prices, predicted_prices, peak_start + 5, 14 - (peak_start + 5), 857 | 0.4, 0.9, 0.03, 0.05); 858 | if (probability == 0) { 859 | return; 860 | } 861 | } 862 | 863 | yield { 864 | pattern_number: 3, 865 | prices: predicted_prices, 866 | probability, 867 | }; 868 | } 869 | 870 | * generate_pattern_3(given_prices) { 871 | for (let peak_start = 2; peak_start < 10; peak_start++) { 872 | yield* this.multiply_generator_probability(this.generate_pattern_3_with_peak(given_prices, peak_start), 1 / (10 - 2)); 873 | } 874 | } 875 | 876 | get_transition_probability(previous_pattern) { 877 | if (typeof previous_pattern === 'undefined' || Number.isNaN(previous_pattern) || previous_pattern === null || previous_pattern < 0 || previous_pattern > 3) { 878 | // Use the steady state probabilities of PROBABILITY_MATRIX if we don't 879 | // know what the previous pattern was. 880 | // See https://github.com/mikebryant/ac-nh-turnip-prices/issues/68 881 | // and https://github.com/mikebryant/ac-nh-turnip-prices/pull/90 882 | // for more information. 883 | return [4530/13082, 3236/13082, 1931/13082, 3385/13082]; 884 | } 885 | 886 | return PROBABILITY_MATRIX[previous_pattern]; 887 | } 888 | 889 | * generate_all_patterns(sell_prices, previous_pattern) { 890 | const generate_pattern_fns = [this.generate_pattern_0, this.generate_pattern_1, this.generate_pattern_2, this.generate_pattern_3]; 891 | const transition_probability = this.get_transition_probability(previous_pattern); 892 | 893 | for (let i = 0; i < 4; i++) { 894 | yield* this.multiply_generator_probability(generate_pattern_fns[i].bind(this)(sell_prices), transition_probability[i]); 895 | } 896 | } 897 | 898 | * generate_possibilities(sell_prices, first_buy, previous_pattern) { 899 | if (first_buy || isNaN(sell_prices[0])) { 900 | for (var buy_price = 90; buy_price <= 110; buy_price++) { 901 | const temp_sell_prices = sell_prices.slice(); 902 | temp_sell_prices[0] = temp_sell_prices[1] = buy_price; 903 | if (first_buy) { 904 | yield* this.generate_pattern_3(temp_sell_prices); 905 | } else { 906 | // All buy prices are equal probability and we're at the outmost layer, 907 | // so don't need to multiply_generator_probability here. 908 | yield* this.generate_all_patterns(temp_sell_prices, previous_pattern); 909 | } 910 | } 911 | } else { 912 | yield* this.generate_all_patterns(sell_prices, previous_pattern); 913 | } 914 | } 915 | 916 | analyze_possibilities() { 917 | const sell_prices = this.prices; 918 | const first_buy = this.first_buy; 919 | const previous_pattern = this.previous_pattern; 920 | let generated_possibilities = []; 921 | for (let i = 0; i < 6; i++) { 922 | this.fudge_factor = i; 923 | generated_possibilities = Array.from(this.generate_possibilities(sell_prices, first_buy, previous_pattern)); 924 | if (generated_possibilities.length > 0) { 925 | console.log("Generated possibilities using fudge factor %d: ", i, generated_possibilities); 926 | break; 927 | } 928 | } 929 | 930 | const total_probability = generated_possibilities.reduce((acc, it) => acc + it.probability, 0); 931 | for (const it of generated_possibilities) { 932 | it.probability /= total_probability; 933 | } 934 | 935 | for (let poss of generated_possibilities) { 936 | var weekMins = []; 937 | var weekMaxes = []; 938 | for (let day of poss.prices.slice(2)) { 939 | // Check for a future date by checking for a range of prices 940 | if(day.min !== day.max){ 941 | weekMins.push(day.min); 942 | weekMaxes.push(day.max); 943 | } else { 944 | // If we find a set price after one or more ranged prices, the user has missed a day. Discard that data and start again. 945 | weekMins = []; 946 | weekMaxes = []; 947 | } 948 | } 949 | if (!weekMins.length && !weekMaxes.length) { 950 | weekMins.push(poss.prices[poss.prices.length -1].min); 951 | weekMaxes.push(poss.prices[poss.prices.length -1].max); 952 | } 953 | poss.weekGuaranteedMinimum = Math.max(...weekMins); 954 | poss.weekMax = Math.max(...weekMaxes); 955 | } 956 | 957 | let category_totals = {}; 958 | for (let i of [0, 1, 2, 3]) { 959 | category_totals[i] = generated_possibilities 960 | .filter(value => value.pattern_number == i) 961 | .map(value => value.probability) 962 | .reduce((previous, current) => previous + current, 0); 963 | } 964 | 965 | for (let pos of generated_possibilities) { 966 | pos.category_total_probability = category_totals[pos.pattern_number]; 967 | } 968 | 969 | generated_possibilities.sort((a, b) => { 970 | return b.category_total_probability - a.category_total_probability || b.probability - a.probability; 971 | }); 972 | 973 | let global_min_max = []; 974 | for (let day = 0; day < 14; day++) { 975 | const prices = { 976 | min: 999, 977 | max: 0, 978 | }; 979 | for (let poss of generated_possibilities) { 980 | if (poss.prices[day].min < prices.min) { 981 | prices.min = poss.prices[day].min; 982 | } 983 | if (poss.prices[day].max > prices.max) { 984 | prices.max = poss.prices[day].max; 985 | } 986 | } 987 | global_min_max.push(prices); 988 | } 989 | 990 | generated_possibilities.unshift({ 991 | pattern_number: 4, 992 | prices: global_min_max, 993 | weekGuaranteedMinimum: Math.min(...generated_possibilities.map(poss => poss.weekGuaranteedMinimum)), 994 | weekMax: Math.max(...generated_possibilities.map(poss => poss.weekMax)) 995 | }); 996 | 997 | return generated_possibilities; 998 | } 999 | } 1000 | --------------------------------------------------------------------------------