├── .browserslistrc ├── .editorconfig ├── .env ├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── SECURITY.md ├── babel.config.js ├── cypress.json ├── package.json ├── postcss.config.js ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── assets │ ├── logo.png │ └── logo.svg ├── components │ ├── DataTable.vue │ ├── Header.vue │ ├── ItemIcon.vue │ ├── MoreDataIndicator.vue │ ├── Settings.vue │ ├── StageCode.vue │ ├── ZoneName.vue │ ├── fact-table │ │ ├── FactTable.vue │ │ └── FactTableItem.vue │ ├── settings │ │ ├── LocaleSwitcher.vue │ │ └── ThemeSwitcher.vue │ └── table │ │ ├── DataTableError.vue │ │ ├── DataTableItem.vue │ │ └── DataTableStage.vue ├── i18n.js ├── locales │ ├── en.yml │ ├── ja.yml │ └── zh.yml ├── main.js ├── mixins │ ├── CDN.js │ ├── Localization.js │ ├── Table.js │ ├── Theme.js │ └── hooks │ │ └── PrefillEnvironment.js ├── plugins │ └── vuetify.js ├── store │ ├── index.js │ └── settings.js ├── styles │ ├── base.css │ └── theme.scss └── utils │ ├── environment.js │ ├── penguin.js │ ├── strings.js │ └── timeFormatter.js ├── tests └── e2e │ ├── .eslintrc.js │ ├── plugins │ └── index.js │ ├── specs │ └── test.js │ └── support │ ├── commands.js │ └── index.js ├── vue.config.js └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 0.01% 2 | last 2 versions 3 | not dead 4 | > 0.1% in CN 5 | 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.{js,jsx,ts,tsx,vue}] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VUE_APP_I18N_LOCALE=en 2 | VUE_APP_I18N_FALLBACK_LOCALE=en 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | globals: { 7 | NPM_PACKAGE_VERSION: true, 8 | GIT_COMMIT: true, 9 | BUILD_TIME: true 10 | }, 11 | extends: [ 12 | 'plugin:vue/recommended', 13 | 'eslint:recommended', 14 | '@vue/standard' 15 | ], 16 | parserOptions: { 17 | parser: 'babel-eslint' 18 | }, 19 | rules: { 20 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 21 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | /tests/e2e/videos/ 6 | /tests/e2e/screenshots/ 7 | 8 | 9 | # local env files 10 | .env.local 11 | .env.*.local 12 | 13 | # Log files 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | pnpm-debug.log* 18 | 19 | # Editor directories and files 20 | .idea 21 | .vscode 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Penguin Statistics 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Penguin Statistics - Logo 4 | 5 | # Penguin Statistics - Widget `frontend` 6 | [![Status](https://img.shields.io/badge/status-production-green)](#readme) 7 | [![Language](https://img.shields.io/badge/using-Vue.js-%234FC08D?logo=vue.js)](#readme) 8 | [![Language](https://img.shields.io/badge/using-Vuetify-%231867C0?logo=vuetify)](#readme) 9 | [![License](https://img.shields.io/github/license/penguin-statistics/widget-frontend)](https://github.com/penguin-statistics/widget-frontend/blob/main/LICENSE) 10 | [![Last Commit](https://img.shields.io/github/last-commit/penguin-statistics/widget-frontend)](https://github.com/penguin-statistics/widget-frontend/commits/main) 11 | 12 | This is the **frontend** project repository for the [Penguin Statistics](https://penguin-stats.io/?utm_source=github) widget. 13 | 14 | ## Maintainers 15 | This project has mainly being maintained by the following contributors (in alphabetical order): 16 | - [GalvinGao](https://github.com/GalvinGao) 17 | 18 | > The full list of active contributors of the *Penguin Statistics* project can be found at the [Team Members page](https://penguin-stats.io/about/members) of the website. 19 | 20 | ## How to contribute? 21 | Our contribute guideline can be found at [Penguin Developers](https://developer.penguin-stats.io). PRs are always more than welcome! 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 1.x | :white_check_mark: | 8 | 9 | ## Reporting a Vulnerability 10 | 11 | First of all, we kindly ask you to **not disclose about the detail of the vulnerability** to the public before we have implemented a working fix that covers the particular vulnerability to protect our users from 0day attacks. 12 | 13 | If you found a vulnerability in particular, the **`widget-frontend`** repository, please do one of the following with your convenience: 14 | - [File a Security Advisory Report (recommended)](https://github.com/penguin-statistics/widget-frontend/security/advisories/new) (that will privately disclose the vulnerability to the moderators of this organization) 15 | - Contact the maintainer of this repository, Galvin Gao, at `me at galvingao dot com` 16 | - Contact the webmaster and organizer of the Penguin Statistics organization, Alviss Reimu, at (`alvissreimu at gmail`). 17 | 18 | Thanks in advance for making Penguin Statistics a better and safer site for everyone! ;) 19 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginsFile": "tests/e2e/plugins/index.js" 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "penguin-stats-widget", 3 | "version": "1.1.2", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "test:e2e": "vue-cli-service test:e2e", 9 | "lint": "vue-cli-service lint", 10 | "i18n:report": "vue-cli-service i18n:report --src './src/**/*.?(js|vue)' --locales './src/locales/**/*.yml'" 11 | }, 12 | "dependencies": { 13 | "@mdi/js": "^5.8.55", 14 | "core-js": "^3.6.5", 15 | "dayjs": "^1.9.6", 16 | "js-yaml-loader": "^1.2.2", 17 | "vue": "^2.6.11", 18 | "vue-gtag": "^1.10.0", 19 | "vue-i18n": "^8.17.3", 20 | "vuetify": "^2.2.11", 21 | "vuex": "^3.4.0", 22 | "vuex-persistedstate": "^4.1.0" 23 | }, 24 | "devDependencies": { 25 | "@vue/cli-plugin-babel": "~4.5.0", 26 | "@vue/cli-plugin-e2e-cypress": "~4.5.0", 27 | "@vue/cli-plugin-eslint": "~4.5.0", 28 | "@vue/cli-plugin-vuex": "~4.5.0", 29 | "@vue/cli-service": "~4.5.0", 30 | "@vue/eslint-config-standard": "^5.1.2", 31 | "babel-eslint": "^10.1.0", 32 | "cssnano": "^4.1.10", 33 | "cssnano-preset-advanced": "^4.0.7", 34 | "cz-conventional-changelog": "^3.3.0", 35 | "eslint": "^6.7.2", 36 | "eslint-plugin-cypress": "^2.11.2", 37 | "eslint-plugin-import": "^2.20.2", 38 | "eslint-plugin-node": "^11.1.0", 39 | "eslint-plugin-promise": "^4.2.1", 40 | "eslint-plugin-standard": "^4.0.0", 41 | "eslint-plugin-vue": "^6.2.2", 42 | "html-webpack-inline-source-plugin": "^0.0.10", 43 | "html-webpack-plugin": "^4.5.0", 44 | "sass": "^1.26.5", 45 | "sass-loader": "^8.0.2", 46 | "vue-cli-plugin-i18n": "~1.0.1", 47 | "vue-cli-plugin-vuetify": "~2.0.7", 48 | "vue-template-compiler": "^2.6.11", 49 | "vuetify-loader": "^1.3.0" 50 | }, 51 | "config": { 52 | "commitizen": { 53 | "path": "./node_modules/cz-conventional-changelog" 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('cssnano')({ 4 | preset: 'default' 5 | }) 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penguin-statistics/widget-frontend/d8bfdda68fde8af99b2c062a83fda7a43cf4d880/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Widget | Penguin Statistics 10 | 11 | 12 | 15 |
16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 83 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/penguin-statistics/widget-frontend/d8bfdda68fde8af99b2c062a83fda7a43cf4d880/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | Artboard 46 2 | -------------------------------------------------------------------------------- /src/components/DataTable.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 28 | 29 | 32 | -------------------------------------------------------------------------------- /src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 69 | 70 | 163 | -------------------------------------------------------------------------------- /src/components/ItemIcon.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 122 | 123 | 141 | -------------------------------------------------------------------------------- /src/components/MoreDataIndicator.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 42 | 43 | 46 | -------------------------------------------------------------------------------- /src/components/Settings.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 24 | 25 | 30 | -------------------------------------------------------------------------------- /src/components/StageCode.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 125 | 126 | 138 | -------------------------------------------------------------------------------- /src/components/ZoneName.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 38 | 39 | 42 | -------------------------------------------------------------------------------- /src/components/fact-table/FactTable.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /src/components/fact-table/FactTableItem.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 48 | 49 | -------------------------------------------------------------------------------- /src/components/settings/LocaleSwitcher.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 70 | 71 | 74 | -------------------------------------------------------------------------------- /src/components/settings/ThemeSwitcher.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 57 | 58 | 61 | -------------------------------------------------------------------------------- /src/components/table/DataTableError.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 67 | 68 | 71 | -------------------------------------------------------------------------------- /src/components/table/DataTableItem.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 99 | 100 | 103 | -------------------------------------------------------------------------------- /src/components/table/DataTableStage.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 84 | 85 | 88 | -------------------------------------------------------------------------------- /src/i18n.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueI18n from 'vue-i18n' 3 | 4 | Vue.use(VueI18n) 5 | 6 | function loadLocaleMessages () { 7 | const locales = require.context('./locales', true, /[A-Za-z0-9-_,\s]+\.ya?ml$/i) 8 | const messages = {} 9 | locales.keys().forEach(key => { 10 | const matched = key.match(/([A-Za-z0-9-_]+)\./i) 11 | if (matched && matched.length > 1) { 12 | const locale = matched[1] 13 | messages[locale] = locales(key) 14 | } 15 | }) 16 | return Object.freeze(messages) 17 | } 18 | 19 | export default new VueI18n({ 20 | locale: process.env.VUE_APP_I18N_LOCALE || 'en', 21 | fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en', 22 | messages: loadLocaleMessages() 23 | }) 24 | -------------------------------------------------------------------------------- /src/locales/en.yml: -------------------------------------------------------------------------------- 1 | app: 2 | name: Widget 3 | vendor: Penguin Statistics 4 | 5 | settings: 6 | _name: Settings 7 | theme: 8 | light: Light Theme 9 | dark: Dark Theme 10 | 11 | copyright: Widget distributed by Penguin Statistics under CC BY-NC 4.0 International License 12 | source: Source (GitHub) 13 | version: "Version {app} (build {git} at {time})" 14 | 15 | errors: 16 | _title: Unable to Load Widget 17 | _sorry: We appologize the inconvenience, but... 18 | _prefix: An error has occurred when loading the widget 19 | _reason: "\"{error}\" has identified as cause" 20 | _reported: Error has been automatically reported to Penguin Statistics 21 | Unknown: Unrecognized Record 22 | TooManyFailures: "Can't fetch {details} after multiple attempts" 23 | InvalidServer: Invalid Server 24 | CantMarshal: Failed to marshal record dependencies 25 | FetchData: Failed to retrieve cache 26 | PageNotFound: Page Not Found 27 | NotFound: Record Not Found with parameter provided 28 | 29 | meta: 30 | separator: ", " 31 | time: 32 | minute: "{m}m" 33 | second: "{s}s" 34 | 35 | title: 36 | stage: "Statistics for Stage {stageName}" 37 | item: "Statistics for Item {itemName}" 38 | exact: "Statistics for {itemName} in {stageName}" 39 | 40 | zone: 41 | types: 42 | MAINLINE: Mainline 43 | WEEKLY: Supplies 44 | ACTIVITY: Event 45 | ACTIVITY_PERMANENT: "Intermezzi & Side Story" 46 | GACHABOX: Gachabox 47 | 48 | table: 49 | scroll: Scroll to view details 50 | headers: 51 | zoneName: Zone 52 | stage: Stage 53 | apCost: Sanity 54 | item: Item 55 | times: Samples 56 | quantity: Loots 57 | percentage: Percentage 58 | apPPR: Sanity Required Per Item 59 | clearTime: Shortest Clear Time 60 | itemPerTime: Time Required Per Item 61 | timeRange: Time Range 62 | timeRange: 63 | inBetween: "{0} to {1}" 64 | toPresent: "{date} to Present" 65 | endsAt: Before {date} 66 | unknown: Unknown 67 | notSelected: "(not selected)" -------------------------------------------------------------------------------- /src/locales/ja.yml: -------------------------------------------------------------------------------- 1 | app: 2 | vendor: ペンギン急便データ統計処理部門 3 | 4 | zone: 5 | types: 6 | MAINLINE: メインステージ 7 | WEEKLY: 物資調達 8 | ACTIVITY: イベント 9 | GACHABOX: 補給物資 10 | 11 | settings: 12 | _name: 設定 13 | theme: 14 | light: ライトモード 15 | dark: ダークモード 16 | -------------------------------------------------------------------------------- /src/locales/zh.yml: -------------------------------------------------------------------------------- 1 | app: 2 | name: 小组件 3 | vendor: 企鹅物流数据统计 4 | 5 | settings: 6 | _name: 设置 7 | theme: 8 | light: 亮色主题 9 | dark: 暗色主题 10 | 11 | copyright: 此小组件数据由企鹅物流数据统计使用知识共享 署名-非商业性使用 4.0 国际 许可协议进行分发 12 | source: 源码 (GitHub) 13 | version: 版本 {app}({time} 的构建版本 {git}) 14 | 15 | errors: 16 | _title: 加载小组件时发生了问题 17 | _sorry: 向阁下带来不便深表歉意,但 18 | _prefix: 加载小组件时发生了问题 19 | _reason: "问题诱因识别为 “{error}”" 20 | _reported: 已自动向企鹅物流数据统计云端报告此次错误 21 | Unknown: 未知的数据格式 22 | TooManyFailures: "屡次无法成功拉取数据项 {details}" 23 | InvalidServer: 不合法的服务器 24 | CantMarshal: 数据依赖处理失败 25 | FetchData: 数据缓存读出失败 26 | PageNotFound: 请求的页面路径不存在 27 | NotFound: 未找到所查询的数据项 28 | 29 | meta: 30 | separator: "、" 31 | time: 32 | minute: "{m}分 " 33 | second: "{s}秒" 34 | 35 | title: 36 | stage: "掉落统计 / {stageName} " 37 | item: "掉落统计 / {itemName}" 38 | exact: "掉落统计 / {stageName} 的 {itemName} " 39 | 40 | zone: 41 | types: 42 | MAINLINE: 主线 43 | WEEKLY: 物资筹备 44 | ACTIVITY: 限时活动 45 | ACTIVITY_PERMANENT: "插曲 & 别传" 46 | GACHABOX: 物资补给箱 47 | 48 | table: 49 | scroll: 左右滑动查看数据 50 | headers: 51 | zoneName: 章节 52 | stage: 作战 53 | apCost: 理智 54 | item: 物品 55 | times: 样本数 56 | quantity: 掉落数 57 | percentage: 百分比 58 | apPPR: 单件估算理智 59 | clearTime: 最短通关用时 60 | itemPerTime: 单件估算用时 61 | timeRange: 统计区间 62 | timeRange: 63 | inBetween: "{0} ~ {1}" 64 | toPresent: "{date} 至今" 65 | endsAt: "{date} 之前" 66 | unknown: 未知 67 | notSelected: "(暂未选择)" -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import store from './store' 4 | import i18n from './i18n' 5 | import vuetify from './plugins/vuetify' 6 | 7 | import '@/styles/base.css' 8 | import '@/styles/theme.scss' 9 | import DataTableError from '@/components/table/DataTableError' 10 | import DataTableItem from '@/components/table/DataTableItem' 11 | import DataTableStage from '@/components/table/DataTableStage' 12 | 13 | import VueGtag from 'vue-gtag' 14 | 15 | Vue.use(VueGtag, { 16 | config: { id: 'G-4VJR7KRK8D' }, 17 | bootstrap: false 18 | }) 19 | 20 | Vue.component('DataTableAuto', { 21 | functional: true, 22 | props: { 23 | options: { 24 | type: Object, 25 | required: true 26 | } 27 | }, 28 | render: function (createElement, ctx) { 29 | let component 30 | let additionalProps = {} 31 | if (ctx.props.options.type === 'item') { 32 | component = DataTableItem 33 | } else if (ctx.props.options.type === 'stage' || ctx.props.options.type === 'exact') { 34 | component = DataTableStage 35 | } else { 36 | component = DataTableError 37 | additionalProps = { 38 | errors: ctx.props.options.errors || [{ 39 | type: 'Unknown' 40 | }] 41 | } 42 | } 43 | 44 | return createElement( 45 | component, 46 | { 47 | props: { 48 | ...ctx.props, 49 | ...additionalProps 50 | } 51 | }, 52 | ctx.children 53 | ) 54 | } 55 | }) 56 | 57 | Vue.config.productionTip = false 58 | 59 | new Vue({ 60 | store, 61 | i18n, 62 | vuetify, 63 | render: h => h(App) 64 | }).$mount('#app') 65 | -------------------------------------------------------------------------------- /src/mixins/CDN.js: -------------------------------------------------------------------------------- 1 | import PenguinData from '@/utils/penguin' 2 | 3 | export default { 4 | methods: { 5 | cdnDeliver (path) { 6 | return PenguinData.mirror().cdn + path 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/mixins/Localization.js: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | import { mapGetters } from 'vuex' 3 | 4 | export default { 5 | methods: { 6 | changeLocale (localeId, save = true) { 7 | dayjs.locale(localeId) 8 | if (save) this.$store.commit('settings/changeLanguage', localeId) 9 | this.$i18n.locale = localeId 10 | } 11 | }, 12 | computed: { 13 | ...mapGetters('settings', ['language']) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/mixins/Table.js: -------------------------------------------------------------------------------- 1 | export default { 2 | computed: { 3 | statHeaders () { 4 | return [ 5 | { 6 | text: this.$t('table.headers.quantity'), 7 | align: 'start', 8 | value: 'quantity', 9 | width: '90px' 10 | }, 11 | { 12 | text: this.$t('table.headers.times'), 13 | align: 'start', 14 | value: 'times', 15 | width: '90px' 16 | }, 17 | { 18 | text: this.$t('table.headers.percentage'), 19 | align: 'start', 20 | value: 'percentage', 21 | width: '100px' 22 | }, 23 | { 24 | text: this.$t('table.headers.apPPR'), 25 | align: 'start', 26 | value: 'apPPR', 27 | width: '130px' 28 | }, 29 | { 30 | text: this.$t('table.headers.timeRange'), 31 | align: 'start', 32 | value: 'timeRange', 33 | sortable: false, 34 | width: '140px' 35 | } 36 | ] 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/mixins/Theme.js: -------------------------------------------------------------------------------- 1 | import { mapGetters } from 'vuex' 2 | import environment from '@/utils/environment' 3 | 4 | export default { 5 | watch: { 6 | theme: ['onThemeChange'] 7 | }, 8 | computed: { 9 | ...mapGetters('settings', ['theme']), 10 | dark () { 11 | return this.theme === 'dark' 12 | } 13 | }, 14 | methods: { 15 | themeToggle (isDark) { 16 | const windowsIndicator = environment.isWindows ? 'platform--windows' : 'platform--not-windows' 17 | this.$vuetify.theme.dark = isDark 18 | const cl = document.documentElement.classList 19 | cl.add(windowsIndicator) 20 | if (isDark) { 21 | cl.remove('vuetify-theme--light') 22 | cl.add('vuetify-theme--dark') 23 | document.body.style.backgroundColor = '#121212' 24 | } else { 25 | cl.remove('vuetify-theme--dark') 26 | cl.add('vuetify-theme--light') 27 | document.body.style.backgroundColor = '#ffffff' 28 | } 29 | }, 30 | onThemeChange (newValue) { 31 | if (newValue === 'dark') { 32 | this.themeToggle(true) 33 | } else { 34 | this.themeToggle(false) 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/mixins/hooks/PrefillEnvironment.js: -------------------------------------------------------------------------------- 1 | import environment from '@/utils/environment' 2 | import Localization from '@/mixins/Localization' 3 | import Theme from '@/mixins/Theme' 4 | import i18n from '@/i18n' 5 | import { bootstrap } from 'vue-gtag' 6 | 7 | export default { 8 | mixins: [Localization, Theme], 9 | created () { 10 | // `lang` parameter 11 | const url = new URL(window.location.href) 12 | const prefillLanguage = url.searchParams.get('lang') || this.language 13 | const detectedLanguage = environment.detectLanguage() 14 | 15 | if (prefillLanguage && ['zh', 'en', 'ja', 'ko'].includes(prefillLanguage)) { 16 | this.changeLocale(prefillLanguage, false) 17 | } else if (detectedLanguage) { 18 | this.changeLocale(detectedLanguage, false) 19 | } else { 20 | this.changeLocale(i18n.fallbackLocale) 21 | } 22 | 23 | const prefillTheme = url.searchParams.get('theme') || this.theme 24 | if (prefillTheme) { 25 | this.onThemeChange(prefillTheme) 26 | } 27 | 28 | const prefillDNT = url.searchParams.get('dnt') === '1' || navigator.doNotTrack === '1' 29 | if (prefillDNT) { 30 | console.info('due to DNT settings, tracking has been disabled') 31 | } else { 32 | bootstrap() 33 | .then(() => { 34 | console.info('vue-gtag initialized') 35 | }) 36 | .catch(() => { 37 | console.error('vue-gtag failed to initialize') 38 | }) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuetify from 'vuetify/lib' 3 | import colors from 'vuetify/lib/util/colors' 4 | 5 | Vue.use(Vuetify) 6 | 7 | export default new Vuetify({ 8 | icons: { 9 | iconfont: 'mdiSvg' 10 | }, 11 | theme: { 12 | themes: { 13 | light: { 14 | background: colors.grey.lighten4 15 | }, 16 | dark: { 17 | background: colors.grey.darken4 18 | } 19 | } 20 | } 21 | }) 22 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import createPersistedState from 'vuex-persistedstate' 4 | 5 | import settings from './settings' 6 | 7 | Vue.use(Vuex) 8 | 9 | export default new Vuex.Store({ 10 | state: { 11 | }, 12 | mutations: { 13 | }, 14 | actions: { 15 | }, 16 | modules: { 17 | settings 18 | }, 19 | plugins: [ 20 | createPersistedState({ 21 | key: 'penguin-stats-widget-settings', 22 | paths: [ 23 | 'settings' 24 | ] 25 | }) 26 | ] 27 | }) 28 | -------------------------------------------------------------------------------- /src/store/settings.js: -------------------------------------------------------------------------------- 1 | export default { 2 | namespaced: true, 3 | state: { 4 | language: 'en', 5 | theme: 'light' 6 | }, 7 | mutations: { 8 | changeLanguage (state, language) { 9 | state.language = language 10 | }, 11 | changeTheme (state, theme) { 12 | state.theme = theme 13 | } 14 | }, 15 | getters: { 16 | language: (state) => state.language, 17 | theme: (state) => state.theme 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/styles/base.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | width: 100%; 4 | padding: 0; 5 | margin: 0; 6 | font-family: 'Helvetica Neue', arial, sans-serif; 7 | font-weight: 400; 8 | color: #444; 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | } 12 | 13 | .v-expansion-panels--inset > .v-expansion-panel--active.v-expansion-panel--dense { 14 | margin-top: 8px; 15 | max-width: calc(100% - 16px) 16 | } 17 | .v-expansion-panel--active.v-expansion-panel--dense > .v-expansion-panel-header { 18 | min-height: 52px 19 | } 20 | 21 | .cursor-pointer { 22 | cursor: pointer; 23 | } 24 | .border-radius-1 { 25 | border-radius: 4px; 26 | } 27 | .no-transform { 28 | text-transform: none !important; 29 | } 30 | .border-1 { 31 | border: 1px solid; 32 | } 33 | .full-width { 34 | width: 100%; 35 | } 36 | .position-relative { 37 | position: relative; 38 | } 39 | .error-box { 40 | border-radius: 8px; 41 | /*content: '';*/ 42 | background-image: linear-gradient(to bottom right, #ff5252, #d42db8) !important; 43 | /*top: -4px;*/ 44 | /*left: -4px;*/ 45 | /*bottom: -4px;*/ 46 | /*right: -4px;*/ 47 | /*position: absolute;*/ 48 | box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), 0px 3px 14px 2px rgba(0, 0, 0, 0.12) !important 49 | } 50 | .monospace { 51 | font-family: 'SF Mono', 'SFMono-Regular', ui-monospace, 'JetBrains Mono', 'Fira Mono', 'Source Code Pro', 'Consolas', monospace; 52 | } 53 | /*.fill-content-height {*/ 54 | /* height: 95%;*/ 55 | /* !*display: flex;*!*/ 56 | /* !*align-items: center;*!*/ 57 | /*}*/ 58 | /*.fill-content-height .v-main__wrap,*/ 59 | /*.fill-content-height .container {*/ 60 | /* height: 100%;*/ 61 | /* display: flex;*/ 62 | /* align-items: center;*/ 63 | /* justify-content: center;*/ 64 | /*}*/ 65 | -------------------------------------------------------------------------------- /src/styles/theme.scss: -------------------------------------------------------------------------------- 1 | // Fix the :( scroll bar ONLY on windows. 2 | 3 | .platform--windows { 4 | &.vuetify-theme--dark { 5 | & ::-webkit-scrollbar { 6 | background-color: #202324; 7 | color: #aba499; 8 | width: 6px; 9 | } 10 | & ::-webkit-scrollbar-thumb { 11 | background-color: #5a5e62; 12 | } 13 | & ::-webkit-scrollbar-thumb:hover { 14 | background-color: #626a6e; 15 | } 16 | & ::-webkit-scrollbar-thumb:active { 17 | background-color: #484e51; 18 | } 19 | & ::-webkit-scrollbar-corner { 20 | background-color: #222526; 21 | } 22 | & * { 23 | scrollbar-color: #202324 #454a4d; 24 | } 25 | } 26 | 27 | &.vuetify-theme--light { 28 | & ::-webkit-scrollbar { 29 | background-color: #aba499; 30 | color: #202324; 31 | width: 6px; 32 | } 33 | & ::-webkit-scrollbar-thumb { 34 | background-color: #5a5e62; 35 | } 36 | & ::-webkit-scrollbar-thumb:hover { 37 | background-color: #737d81; 38 | } 39 | & ::-webkit-scrollbar-thumb:active { 40 | background-color: #363b3c; 41 | } 42 | & ::-webkit-scrollbar-corner { 43 | background-color: #222526; 44 | } 45 | & * { 46 | scrollbar-color: #c2cfd6 #ddebf1; 47 | } 48 | } 49 | } 50 | 51 | .vuetify-theme--dark { 52 | input:-webkit-autofill, 53 | textarea:-webkit-autofill, 54 | select:-webkit-autofill { 55 | background-color: #555b00 !important; 56 | color: #e8e6e3 !important; 57 | } 58 | ::selection { 59 | background-color: #004daa !important; 60 | color: #e8e6e3 !important; 61 | } 62 | ::-moz-selection { 63 | background-color: #004daa !important; 64 | color: #e8e6e3 !important; 65 | } 66 | } 67 | 68 | .vuetify-theme--light { 69 | ::selection { 70 | background-color: #b3d4fc !important; 71 | color: #000 !important; 72 | } 73 | ::-moz-selection { 74 | background-color: #b3d4fc !important; 75 | color: #000 !important; 76 | } 77 | } 78 | 79 | html, body, main, .transition-all { 80 | transition-timing-function: cubic-bezier(0.165, 0.84, 0.44, 1); 81 | } 82 | html, body, #app { 83 | transition: background .125s; 84 | } 85 | main { 86 | transition: all .125s; 87 | } 88 | .transition-all { 89 | transition: all .225s; 90 | } 91 | 92 | .settings-option { 93 | transition: all .225s; 94 | transition-delay: 0s; 95 | &:hover { 96 | background: rgba(18, 18, 18, .18); 97 | } 98 | &:not(.settings-option--active) { 99 | &:hover { 100 | box-shadow: 0 0 5px rgba(0, 0, 0, .4); 101 | } 102 | &:active { 103 | box-shadow: 0 0 2px rgba(0, 0, 0, .35); 104 | } 105 | } 106 | .vuetify-theme--dark &:hover { 107 | background: rgba(255, 255, 255, .18); 108 | } 109 | } 110 | .data-table { 111 | table { 112 | //position: relative; 113 | } 114 | } 115 | .item-icon { 116 | position: absolute !important; 117 | 118 | left: 12px; 119 | filter: drop-shadow(0 0 4px rgba(0, 0, 0, .28)); 120 | } 121 | 122 | .item-name { 123 | padding-left: 38px; 124 | } 125 | -------------------------------------------------------------------------------- /src/utils/environment.js: -------------------------------------------------------------------------------- 1 | import i18n from '@/i18n' 2 | 3 | function getFirstBrowserLanguageWithRegionCode () { 4 | const nav = window.navigator 5 | const browserLanguagePropertyKeys = ['language', 'browserLanguage', 'systemLanguage', 'userLanguage'] 6 | let i 7 | let language 8 | let len 9 | let shortLanguage = null 10 | 11 | // support for HTML 5.1 "navigator.languages" 12 | if (Array.isArray(nav.languages)) { 13 | for (i = 0; i < nav.languages.length; i++) { 14 | language = nav.languages[i] 15 | len = language.length 16 | if (!shortLanguage && len) { 17 | shortLanguage = language 18 | } 19 | if (language && len > 2) { 20 | return language 21 | } 22 | } 23 | } 24 | 25 | // support for other well known properties in browsers 26 | for (i = 0; i < browserLanguagePropertyKeys.length; i++) { 27 | language = nav[browserLanguagePropertyKeys[i]] 28 | // skip this loop iteration if property is null/undefined. IE11 fix. 29 | if (language == null) { continue } 30 | len = language.length 31 | if (!shortLanguage && len) { 32 | shortLanguage = language 33 | } 34 | if (language && len > 2) { 35 | return language 36 | } 37 | } 38 | 39 | return shortLanguage 40 | } 41 | 42 | function detectLanguage () { 43 | const language = getFirstBrowserLanguageWithRegionCode().replace('_', '-') 44 | if (!language) return i18n.fallbackLocale // use default 45 | return language.split('-')[0] 46 | } 47 | 48 | export default { 49 | detectLanguage 50 | } 51 | -------------------------------------------------------------------------------- /src/utils/penguin.js: -------------------------------------------------------------------------------- 1 | import i18n from '@/i18n' 2 | import strings from '@/utils/strings' 3 | 4 | const VALID_FAILS = 5 5 | 6 | const PenguinData = { 7 | _cache: null 8 | } 9 | 10 | function validate (data) { 11 | const errors = [] 12 | if (data.error) { 13 | errors.push(data.error) 14 | return errors 15 | } 16 | for (const [key, value] of Object.entries(data.cache)) { 17 | if (value.failures && value.failures > VALID_FAILS) { 18 | errors.push({ 19 | type: 'TooManyFailures', 20 | details: key 21 | }) 22 | } 23 | } 24 | return errors 25 | } 26 | 27 | PenguinData.raw = function () { 28 | // eslint-disable-next-line 29 | /* # by ItemID # */ // const raw = {"request":{},"query":{"itemId":"30012","server":"CN"},"cache":{"item":{"updated":"2022-05-10T10:22:50.035720712Z"},"matrix":{"updated":"2022-05-10T10:32:50.133459175Z"},"siteStats":{"updated":"2022-05-10T10:32:50.456049889Z"},"stage":{"updated":"2022-05-10T10:22:50.297206227Z"},"zone":{"updated":"2022-05-10T10:22:50.438436509Z"}},"items":[{"itemId":"30012","name_i18n":{"en":"Orirock Cube","ja":"初級源岩","ko":"원암 큐브","zh":"固源岩"},"spriteCoord":[5,0]}],"matrix":[{"stageId":"randomMaterial_3","itemId":"30012","quantity":5325,"times":30724,"start":1589529600000},{"stageId":"randomMaterial_4","itemId":"30012","quantity":2529,"times":14477,"start":1604217600000},{"stageId":"main_03-05","itemId":"30012","quantity":17,"times":69,"start":1577174400000},{"stageId":"sub_02-10","itemId":"30012","quantity":2,"times":75,"start":1556676000000},{"stageId":"a001_03_perm","itemId":"30012","quantity":1,"times":14,"start":1618430400000},{"stageId":"tough_10-06","itemId":"30012","quantity":423,"times":1243,"start":1649923200000},{"stageId":"act11d0_04","itemId":"30012","quantity":330,"times":385,"start":1594281600000,"end":1595448000000},{"stageId":"main_04-01","itemId":"30012","quantity":139,"times":515,"start":1577174400000},{"stageId":"main_02-06","itemId":"30012","quantity":244,"times":9123,"start":1556676000000},{"stageId":"sub_02-05","itemId":"30012","quantity":12,"times":590,"start":1556676000000},{"stageId":"act4d0_04","itemId":"30012","quantity":50,"times":239,"start":1571126400000,"end":1571688000000},{"stageId":"main_02-04","itemId":"30012","quantity":246,"times":8858,"start":1556676000000},{"stageId":"act13side_04","itemId":"30012","quantity":386,"times":448,"start":1635753600000,"end":1637524800000},{"stageId":"main_02-03","itemId":"30012","quantity":23,"times":717,"start":1556676000000},{"stageId":"main_04-06","itemId":"30012","quantity":8247,"times":26276,"start":1577174400000},{"stageId":"act18d3_04_perm","itemId":"30012","quantity":0,"times":5,"start":1649923200000},{"stageId":"act7mini_03","itemId":"30012","quantity":2,"times":118,"start":1622534400000,"end":1623096000000},{"stageId":"act18d3_06","itemId":"30012","quantity":565,"times":644,"start":1619856000000,"end":1621022400000},{"stageId":"act15side_04","itemId":"30012","quantity":3716,"times":4322,"start":1643097600000,"end":1644264000000},{"stageId":"main_04-10","itemId":"30012","quantity":6054,"times":16864,"start":1577174400000},{"stageId":"main_05-01","itemId":"30012","quantity":261,"times":869,"start":1577174400000},{"stageId":"sub_03-3-2","itemId":"30012","quantity":2377,"times":1116,"start":1604217600000},{"stageId":"main_03-03","itemId":"30012","quantity":7287,"times":34790,"start":1556676000000},{"stageId":"main_04-03","itemId":"30012","quantity":25,"times":92,"start":1577174400000},{"stageId":"act13d0_05","itemId":"30012","quantity":1,"times":92,"start":1600934400000,"end":1601496000000},{"stageId":"act16d5_06","itemId":"30012","quantity":1165,"times":694,"start":1612512000000,"end":1613678400000},{"stageId":"tough_10-15","itemId":"30012","quantity":170,"times":506,"start":1649923200000},{"stageId":"main_08-01","itemId":"30012","quantity":84,"times":309,"start":1604217600000},{"stageId":"act6d5_03","itemId":"30012","quantity":220,"times":132,"start":1578513600000,"end":1579118400000},{"stageId":"main_10-05","itemId":"30012","quantity":703,"times":1877,"start":1649923200000},{"stageId":"act15d0_04_rep","itemId":"30012","quantity":135,"times":161,"start":1638864000000,"end":1639684800000},{"stageId":"a003_f03_perm","itemId":"30012","quantity":221,"times":971,"start":1618430400000},{"stageId":"act9d0_05_perm","itemId":"30012","quantity":354,"times":159,"start":1618430400000},{"stageId":"act16side_04","itemId":"30012","quantity":15108,"times":18135,"start":1647331200000,"end":1648497600000},{"stageId":"act16d5_06_rep","itemId":"30012","quantity":1105,"times":670,"start":1641801600000,"end":1642622400000},{"stageId":"act15d0_04","itemId":"30012","quantity":113,"times":140,"start":1608192000000,"end":1609358400000},{"stageId":"sub_03-1-2","itemId":"30012","quantity":612,"times":2953,"start":1556676000000},{"stageId":"act18d0_05_perm","itemId":"30012","quantity":0,"times":1,"start":1633032000000},{"stageId":"gachabox6","itemId":"30012","quantity":988,"times":19913,"start":1566892800000,"end":1568664000000},{"stageId":"main_01-07","itemId":"30012","quantity":1544197,"times":1239847,"start":1556676000000},{"stageId":"main_08-14","itemId":"30012","quantity":369,"times":1129,"start":1604217600000},{"stageId":"main_09-07","itemId":"30012","quantity":308,"times":975,"start":1631865600000},{"stageId":"main_09-11","itemId":"30012","quantity":570,"times":1955,"start":1631865600000},{"stageId":"act18d0_08_perm","itemId":"30012","quantity":263,"times":848,"start":1620244800000},{"stageId":"act9d0_04_rep","itemId":"30012","quantity":87,"times":54,"start":1617091200000,"end":1617912000000},{"stageId":"act9d0_04","itemId":"30012","quantity":3029,"times":1819,"start":1587456000000,"end":1589486400000},{"stageId":"act15d5_06","itemId":"30012","quantity":127,"times":4983,"start":1609833600000,"end":1610395200000},{"stageId":"main_05-05","itemId":"30012","quantity":1992,"times":6298,"start":1577174400000},{"stageId":"sub_03-3-1","itemId":"30012","quantity":4208,"times":19808,"start":1577174400000},{"stageId":"randomMaterial_6","itemId":"30012","quantity":659,"times":5382,"start":1649923200000},{"stageId":"main_06-03","itemId":"30012","quantity":193,"times":589,"start":1577174400000},{"stageId":"main_10-01","itemId":"30012","quantity":4,"times":26,"start":1649923200000},{"stageId":"act16d5_04","itemId":"30012","quantity":318,"times":379,"start":1612512000000,"end":1613678400000},{"stageId":"act10mini_03","itemId":"30012","quantity":0,"times":151,"start":1644912000000,"end":1645473600000},{"stageId":"act13d5_02_perm","itemId":"30012","quantity":9,"times":7,"start":1634241600000},{"stageId":"act12d0_09_perm","itemId":"30012","quantity":35,"times":151,"start":1627934400000},{"stageId":"act10mini_05","itemId":"30012","quantity":84,"times":379,"start":1644912000000,"end":1645473600000},{"stageId":"act4d0_05","itemId":"30012","quantity":6779,"times":21512,"start":1571126400000,"end":1571688000000},{"stageId":"randomMaterial_5","itemId":"30012","quantity":1812,"times":11397,"start":1631865600000},{"stageId":"sub_05-1-1","itemId":"30012","quantity":545,"times":327,"start":1562659200000},{"stageId":"act9mini_04","itemId":"30012","quantity":1,"times":38,"start":1634284800000,"end":1634846400000},{"stageId":"act6d5_04","itemId":"30012","quantity":0,"times":38,"start":1578513600000,"end":1579118400000},{"stageId":"sub_04-4-1","itemId":"30012","quantity":1745,"times":5510,"start":1577174400000},{"stageId":"sub_07-1-1","itemId":"30012","quantity":67,"times":233,"start":1588320000000},{"stageId":"act18d0_06_perm","itemId":"30012","quantity":123,"times":633,"start":1620244800000},{"stageId":"act12d0_05_perm","itemId":"30012","quantity":0,"times":4,"start":1627934400000},{"stageId":"act17side_06","itemId":"30012","quantity":1470,"times":1428,"start":1651392000000,"end":1653163200000},{"stageId":"act16d5_08_perm","itemId":"30012","quantity":17,"times":60,"start":1642968000000},{"stageId":"main_08-13","itemId":"30012","quantity":5302,"times":14386,"start":1604217600000},{"stageId":"act17d0_04","itemId":"30012","quantity":143,"times":164,"start":1615276800000,"end":1616443200000},{"stageId":"act18d0_01_perm","itemId":"30012","quantity":0,"times":3,"start":1633032000000},{"stageId":"a001_05_perm","itemId":"30012","quantity":1460,"times":6769,"start":1618430400000},{"stageId":"sub_02-12","itemId":"30012","quantity":27517,"times":12013,"start":1556676000000},{"stageId":"act13d5_05","itemId":"30012","quantity":285,"times":266,"start":1602748800000,"end":1603915200000},{"stageId":"randomMaterial_1","itemId":"30012","quantity":1289,"times":18127,"start":1577174400000},{"stageId":"main_03-04","itemId":"30012","quantity":7150,"times":33983,"start":1556676000000},{"stageId":"act16d5_10_perm","itemId":"30012","quantity":327,"times":1074,"start":1642968000000},{"stageId":"main_07-11","itemId":"30012","quantity":71,"times":278,"start":1588320000000},{"stageId":"act12d0_04_rep","itemId":"30012","quantity":97,"times":123,"start":1626768000000,"end":1627588800000},{"stageId":"act11d0_05_perm","itemId":"30012","quantity":2,"times":10,"start":1633032000000},{"stageId":"sub_02-07","itemId":"30012","quantity":11,"times":413,"start":1556676000000},{"stageId":"act17d0_06","itemId":"30012","quantity":379,"times":455,"start":1615276800000,"end":1616443200000},{"stageId":"main_03-02","itemId":"30012","quantity":12362,"times":59091,"start":1556676000000},{"stageId":"act7d5_03","itemId":"30012","quantity":0,"times":53,"start":1582617600000,"end":1583179200000},{"stageId":"act18d3_04","itemId":"30012","quantity":194,"times":224,"start":1619856000000,"end":1621022400000},{"stageId":"main_08-06","itemId":"30012","quantity":30,"times":75,"start":1604217600000},{"stageId":"main_09-12","itemId":"30012","quantity":92,"times":246,"start":1631865600000},{"stageId":"act13d0_06","itemId":"30012","quantity":54,"times":2397,"start":1600934400000,"end":1601496000000},{"stageId":"act11d0_01_perm","itemId":"30012","quantity":26,"times":21,"start":1625083200000},{"stageId":"main_05-10","itemId":"30012","quantity":47473,"times":25599,"start":1562659200000},{"stageId":"a001_01_perm","itemId":"30012","quantity":37,"times":34,"start":1618430400000},{"stageId":"act15d5_05","itemId":"30012","quantity":0,"times":38,"start":1609833600000,"end":1610395200000},{"stageId":"act15d0_07_perm","itemId":"30012","quantity":58,"times":190,"start":1640030400000},{"stageId":"main_05-02","itemId":"30012","quantity":473,"times":1492,"start":1577174400000},{"stageId":"act18d0_04","itemId":"30012","quantity":552,"times":666,"start":1618473600000,"end":1619640000000},{"stageId":"act15d0_05_perm","itemId":"30012","quantity":1,"times":3,"start":1640030400000},{"stageId":"main_10-06","itemId":"30012","quantity":307,"times":975,"start":1649923200000},{"stageId":"act11d0_08_perm","itemId":"30012","quantity":297,"times":872,"start":1625083200000},{"stageId":"main_02-09","itemId":"30012","quantity":168,"times":5815,"start":1556676000000},{"stageId":"main_10-09","itemId":"30012","quantity":32,"times":91,"start":1649923200000},{"stageId":"act9d0_03_perm","itemId":"30012","quantity":2,"times":44,"start":1618430400000},{"stageId":"act10d5_04","itemId":"30012","quantity":3,"times":81,"start":1592467200000,"end":1593028800000},{"stageId":"act13side_06","itemId":"30012","quantity":640,"times":615,"start":1635753600000,"end":1637524800000},{"stageId":"main_06-12","itemId":"30012","quantity":372,"times":1255,"start":1577174400000},{"stageId":"act18d3_08_perm","itemId":"30012","quantity":218,"times":644,"start":1621627200000},{"stageId":"sub_05-4-2","itemId":"30012","quantity":1972,"times":6297,"start":1577174400000},{"stageId":"randomMaterial_2","itemId":"30012","quantity":806,"times":12969,"start":1581105600000},{"stageId":"main_04-04","itemId":"30012","quantity":11194,"times":35417,"start":1577174400000},{"stageId":"sub_03-2-2","itemId":"30012","quantity":2023,"times":10160,"start":1556676000000},{"stageId":"main_09-15","itemId":"30012","quantity":495,"times":1596,"start":1631865600000},{"stageId":"a003_f02_perm","itemId":"30012","quantity":0,"times":11,"start":1618430400000},{"stageId":"act16d5_04_rep","itemId":"30012","quantity":196,"times":231,"start":1641801600000,"end":1642622400000},{"stageId":"act11d0_04_rep","itemId":"30012","quantity":142,"times":174,"start":1623916800000,"end":1624737600000},{"stageId":"sub_06-1-2","itemId":"30012","quantity":448,"times":276,"start":1577174400000},{"stageId":"main_05-09","itemId":"30012","quantity":86,"times":256,"start":1577174400000},{"stageId":"act13d5_05_rep","itemId":"30012","quantity":87,"times":80,"start":1633075200000,"end":1633896000000},{"stageId":"main_07-14","itemId":"30012","quantity":34381,"times":33501,"start":1588320000000},{"stageId":"main_09-03","itemId":"30012","quantity":3097,"times":10439,"start":1631865600000},{"stageId":"act18d0_04_rep","itemId":"30012","quantity":2622,"times":3067,"start":1648540800000,"end":1649361600000},{"stageId":"main_04-07","itemId":"30012","quantity":10943,"times":34992,"start":1577174400000},{"stageId":"act10mini_07","itemId":"30012","quantity":2536,"times":7149,"start":1644912000000,"end":1645473600000},{"stageId":"main_10-15","itemId":"30012","quantity":48,"times":157,"start":1649923200000},{"stageId":"tough_10-09","itemId":"30012","quantity":23,"times":57,"start":1649923200000},{"stageId":"tough_10-01","itemId":"30012","quantity":9,"times":31,"start":1649923200000},{"stageId":"act14side_04","itemId":"30012","quantity":1354,"times":1622,"start":1640073600000,"end":1641240000000},{"stageId":"tough_10-05","itemId":"30012","quantity":1420,"times":3879,"start":1649923200000},{"stageId":"act8mini_05","itemId":"30012","quantity":32,"times":1036,"start":1626163200000,"end":1626724800000},{"stageId":"act12d0_04","itemId":"30012","quantity":89,"times":107,"start":1598342400000,"end":1599508800000}],"stages":[{"zoneId":"act15d0_zone1","stageId":"act15d0_04","code_i18n":{"en":"MB-4","ja":"MB-4","ko":"MB-4","zh":"MB-4"},"apCost":12,"minClearTime":169000},{"zoneId":"permanent_sidestory_2_zone1","stageId":"a003_f02_perm","code_i18n":{"en":"OF-F2","ja":"OF-F2","ko":"OF-F2","zh":"OF-F2"},"apCost":12,"minClearTime":181000},{"zoneId":"act15d5_zone1","stageId":"act15d5_06","code_i18n":{"en":"BH-6","ja":"BH-6","ko":"BH-6","zh":"BH-6"},"apCost":12,"minClearTime":218000},{"zoneId":"permanent_sidestory_5_zone1","stageId":"act12d0_05_perm","code_i18n":{"en":"RI-5","ja":"RI-5","ko":"RI-5","zh":"RI-5"},"apCost":15,"minClearTime":175000},{"zoneId":"main_9","stageId":"main_09-15","code_i18n":{"en":"9-17","ja":"9-17","ko":"9-17","zh":"9-17"},"apCost":18,"minClearTime":182000},{"zoneId":"main_2","stageId":"main_02-04","code_i18n":{"en":"2-4","ja":"2-4","ko":"2-4","zh":"2-4"},"apCost":12,"minClearTime":149000},{"zoneId":"main_4","stageId":"sub_04-4-1","code_i18n":{"en":"S4-10","ja":"S4-10","ko":"S4-10","zh":"S4-10"},"apCost":18,"minClearTime":129000},{"zoneId":"act17side_zone1","stageId":"act17side_06","code_i18n":{"en":"SN-6","ja":"SN-6","ko":"SN-6","zh":"SN-6"},"apCost":15,"minClearTime":185000},{"zoneId":"main_2","stageId":"main_02-09","code_i18n":{"en":"2-9","ja":"2-9","ko":"2-9","zh":"2-9"},"apCost":12,"minClearTime":193000},{"zoneId":"act10d5_zone1","stageId":"act10d5_04","code_i18n":{"en":"SV-4","ja":"SV-4","ko":"SV-4","zh":"SV-4"},"apCost":12,"minClearTime":219000},{"zoneId":"permanent_sub_3_zone1","stageId":"act18d3_04_perm","code_i18n":{"en":"SV-4","ja":"SV-4","ko":"SV-4","zh":"SV-4"},"apCost":12,"minClearTime":213000},{"zoneId":"act17d5_zone1","stageId":"act9d0_04_rep","code_i18n":{"en":"DM-4","ja":"DM-4","ko":"DM-4","zh":"DM-4"},"apCost":12,"minClearTime":141000},{"zoneId":"act9sre_zone1","stageId":"act16d5_04_rep","code_i18n":{"en":"WR-4","ja":"WR-4","ko":"WR-4","zh":"WR-4"},"apCost":12,"minClearTime":205000},{"zoneId":"main_2","stageId":"main_02-03","code_i18n":{"en":"2-3","ja":"2-3","ko":"2-3","zh":"2-3"},"apCost":12,"minClearTime":218700},{"zoneId":"main_5","stageId":"main_05-02","code_i18n":{"en":"5-2","ja":"5-2","ko":"5-2","zh":"5-2"},"apCost":18,"minClearTime":187000},{"zoneId":"main_5","stageId":"main_05-10","code_i18n":{"en":"5-10","ja":"5-10","ko":"5-10","zh":"5-10"},"apCost":21,"minClearTime":337000},{"zoneId":"main_10_tough","stageId":"tough_10-01","code_i18n":{"en":"10-2","ja":"10-2","ko":"10-2","zh":"10-2"},"apCost":21,"minClearTime":170000},{"zoneId":"act16d5_zone1","stageId":"act16d5_04","code_i18n":{"en":"WR-4","ja":"WR-4","ko":"WR-4","zh":"WR-4"},"apCost":12,"minClearTime":205000},{"zoneId":"permanent_sub_2_zone1","stageId":"act18d0_06_perm","code_i18n":{"en":"WD-6","ja":"WD-6","ko":"WD-6","zh":"WD-6"},"apCost":15,"minClearTime":195000},{"zoneId":"main_5","stageId":"sub_05-4-2","code_i18n":{"en":"S5-8","ja":"S5-8","ko":"S5-8","zh":"S5-8"},"apCost":18,"minClearTime":148000},{"zoneId":"act14side_zone1","stageId":"act14side_04","code_i18n":{"en":"BI-4","ja":"BI-4","ko":"BI-4","zh":"BI-4"},"apCost":12,"minClearTime":138000},{"zoneId":"gachabox","stageId":"randomMaterial_4","code_i18n":{"en":"Fan Appreciation Supplies","ja":"補給物資・感謝祭","ko":"감사 축제 보급 물자","zh":"感谢庆典物资补给"},"apCost":99},{"zoneId":"act15side_zone1","stageId":"act15side_04","code_i18n":{"en":"IW-4","ja":"IW-4","ko":"IW-4","zh":"IW-4"},"apCost":12,"minClearTime":181000},{"zoneId":"act10sre_zone1","stageId":"act18d0_04_rep","code_i18n":{"en":"WD-4","ja":"WD-4","ko":"WD-4","zh":"WD-4"},"apCost":12,"minClearTime":195000},{"zoneId":"act16d5_zone1","stageId":"act16d5_06","code_i18n":{"en":"WR-6","ja":"WR-6","ko":"WR-6","zh":"WR-6"},"apCost":12,"minClearTime":169000},{"zoneId":"gachabox","stageId":"randomMaterial_5","code_i18n":{"en":"Rhodes Island Supplies II","ja":"補給物資・ロドス II","ko":"로도스 아일랜드 보급 물자 II","zh":"罗德岛物资补给II"},"apCost":99},{"zoneId":"act13d5_zone1","stageId":"act13d5_05","code_i18n":{"en":"MN-5","ja":"MN-5","ko":"MN-5","zh":"MN-5"},"apCost":15,"minClearTime":150000},{"zoneId":"main_2","stageId":"sub_02-05","code_i18n":{"en":"S2-5","ja":"S2-5","ko":"S2-5","zh":"S2-5"},"apCost":12,"minClearTime":178000},{"zoneId":"main_9","stageId":"main_09-07","code_i18n":{"en":"9-9","ja":"9-9","ko":"9-9","zh":"9-9"},"apCost":18,"minClearTime":204000},{"zoneId":"main_4","stageId":"main_04-06","code_i18n":{"en":"4-6","ja":"4-6","ko":"4-6","zh":"4-6"},"apCost":18,"minClearTime":198000},{"zoneId":"permanent_sub_2_zone1","stageId":"act18d0_08_perm","code_i18n":{"en":"WD-8","ja":"WD-8","ko":"WD-8","zh":"WD-8"},"apCost":18,"minClearTime":145000},{"zoneId":"main_4","stageId":"main_04-07","code_i18n":{"en":"4-7","ja":"4-7","ko":"4-7","zh":"4-7"},"apCost":18,"minClearTime":200000},{"zoneId":"permanent_sidestory_1_zone1","stageId":"a001_03_perm","code_i18n":{"en":"GT-3","ja":"GT-3","ko":"GT-3","zh":"GT-3"},"apCost":12,"minClearTime":213000},{"zoneId":"main_2","stageId":"main_02-06","code_i18n":{"en":"2-6","ja":"2-6","ko":"2-6","zh":"2-6"},"apCost":12,"minClearTime":174000},{"zoneId":"act9mini_zone1","stageId":"act9mini_04","code_i18n":{"en":"PS-4","ja":"PS-4","ko":"PS-4","zh":"PS-4"},"apCost":12,"minClearTime":190000},{"zoneId":"main_3","stageId":"main_03-04","code_i18n":{"en":"3-4","ja":"3-4","ko":"3-4","zh":"3-4"},"apCost":15,"minClearTime":216400},{"zoneId":"permanent_sidestory_7_zone1","stageId":"act15d0_05_perm","code_i18n":{"en":"MB-5","ja":"MB-5","ko":"MB-5","zh":"MB-5"},"apCost":15,"minClearTime":163000},{"zoneId":"act13side_zone1","stageId":"act13side_06","code_i18n":{"en":"NL-6","ja":"NL-6","ko":"NL-6","zh":"NL-6"},"apCost":15,"minClearTime":175000},{"zoneId":"gachabox","stageId":"randomMaterial_2","code_i18n":{"en":"New Year's Lantern","ja":"年関ランタン","ko":"축제 등불","zh":"岁过华灯"},"apCost":99},{"zoneId":"act5sre_zone1","stageId":"act11d0_04_rep","code_i18n":{"en":"TW-4","ja":"TW-4","ko":"TW-4","zh":"TW-4"},"apCost":12,"minClearTime":175000},{"zoneId":"main_4","stageId":"main_04-03","code_i18n":{"en":"4-3","ja":"4-3","ko":"4-3","zh":"4-3"},"apCost":18,"minClearTime":217500},{"zoneId":"main_9","stageId":"main_09-11","code_i18n":{"en":"9-13","ja":"9-13","ko":"9-13","zh":"9-13"},"apCost":18,"minClearTime":184000},{"zoneId":"act6d5_zone1","stageId":"act6d5_04","code_i18n":{"en":"AF-4","ja":"AF-4","ko":"AF-4","zh":"AF-4"},"apCost":12,"minClearTime":177000},{"zoneId":"act12d0_zone1","stageId":"act12d0_04","code_i18n":{"en":"RI-4","ja":"RI-4","ko":"RI-4","zh":"RI-4"},"apCost":12,"minClearTime":169000},{"zoneId":"act11d0_zone1","stageId":"act11d0_04","code_i18n":{"en":"TW-4","ja":"TW-4","ko":"TW-4","zh":"TW-4"},"apCost":12,"minClearTime":175000},{"zoneId":"main_4","stageId":"main_04-01","code_i18n":{"en":"4-1","ja":"4-1","ko":"4-1","zh":"4-1"},"apCost":18,"minClearTime":201000},{"zoneId":"main_8","stageId":"main_08-14","code_i18n":{"en":"M8-8","ja":"M8-8","ko":"M8-8","zh":"M8-8"},"apCost":18,"minClearTime":257000},{"zoneId":"act13side_zone1","stageId":"act13side_04","code_i18n":{"en":"NL-4","ja":"NL-4","ko":"NL-4","zh":"NL-4"},"apCost":12,"minClearTime":153000},{"zoneId":"act7mini_zone1","stageId":"act7mini_03","code_i18n":{"en":"PL-3","ja":"PL-3","ko":"PL-3","zh":"PL-3"},"apCost":12,"minClearTime":187000},{"zoneId":"main_3","stageId":"sub_03-1-2","code_i18n":{"en":"S3-2","ja":"S3-2","ko":"S3-2","zh":"S3-2"},"apCost":15,"minClearTime":125000},{"zoneId":"main_2","stageId":"sub_02-12","code_i18n":{"en":"S2-12","ja":"S2-12","ko":"S2-12","zh":"S2-12"},"apCost":15,"minClearTime":135000},{"zoneId":"act18d3_zone1","stageId":"act18d3_04","code_i18n":{"en":"SV-4","ja":"SV-4","ko":"SV-4","zh":"SV-4"},"apCost":12,"minClearTime":213000},{"zoneId":"main_7","stageId":"main_07-14","code_i18n":{"en":"7-16","ja":"7-16","ko":"7-16","zh":"7-16"},"apCost":18,"minClearTime":195000},{"zoneId":"act4d0_zone1","stageId":"act4d0_04","code_i18n":{"en":"SW-EV-4","ja":"SW-EV-4","ko":"SW-EV-4","zh":"SW-EV-4"},"apCost":15,"minClearTime":166000},{"zoneId":"main_5","stageId":"main_05-01","code_i18n":{"en":"5-1","ja":"5-1","ko":"5-1","zh":"5-1"},"apCost":18,"minClearTime":166000},{"zoneId":"main_5","stageId":"main_05-05","code_i18n":{"en":"5-5","ja":"5-5","ko":"5-5","zh":"5-5"},"apCost":18,"minClearTime":193000},{"zoneId":"act18d0_zone1","stageId":"act18d0_04","code_i18n":{"en":"WD-4","ja":"WD-4","ko":"WD-4","zh":"WD-4"},"apCost":12,"minClearTime":195000},{"zoneId":"permanent_sub_2_zone1","stageId":"act18d0_01_perm","code_i18n":{"en":"WD-1","ja":"WD-1","ko":"WD-1","zh":"WD-1"},"apCost":12,"minClearTime":194000},{"zoneId":"act15d5_zone1","stageId":"act15d5_05","code_i18n":{"en":"BH-5","ja":"BH-5","ko":"BH-5","zh":"BH-5"},"apCost":12,"minClearTime":262000},{"zoneId":"main_6","stageId":"main_06-12","code_i18n":{"en":"6-14","ja":"6-14","ko":"6-14","zh":"6-14"},"apCost":18,"minClearTime":194000},{"zoneId":"act3d0_zone1","stageId":"gachabox6","code_i18n":{"en":"Gacha Machine","ja":"ガチャマシン","ko":"Gacha Machine","zh":"奖励扭蛋机"},"apCost":20},{"zoneId":"main_1","stageId":"main_01-07","code_i18n":{"en":"1-7","ja":"1-7","ko":"1-7","zh":"1-7"},"apCost":6,"minClearTime":118000},{"zoneId":"main_6","stageId":"sub_06-1-2","code_i18n":{"en":"S6-2","ja":"S6-2","ko":"S6-2","zh":"S6-2"},"apCost":18,"minClearTime":203000},{"zoneId":"act18d3_zone1","stageId":"act18d3_06","code_i18n":{"en":"SV-6","ja":"SV-6","ko":"SV-6","zh":"SV-6"},"apCost":12,"minClearTime":181000},{"zoneId":"permanent_sub_1_zone1","stageId":"act9d0_05_perm","code_i18n":{"en":"DM-5","ja":"DM-5","ko":"DM-5","zh":"DM-5"},"apCost":15,"minClearTime":132000},{"zoneId":"act17d0_zone1","stageId":"act17d0_06","code_i18n":{"en":"OD-6","ja":"OD-6","ko":"OD-6","zh":"OD-6"},"apCost":12,"minClearTime":197000},{"zoneId":"main_10","stageId":"main_10-09","code_i18n":{"en":"10-10","ja":"10-10","ko":"10-10","zh":"10-10"},"apCost":21,"minClearTime":188000},{"zoneId":"main_4","stageId":"main_04-04","code_i18n":{"en":"4-4","ja":"4-4","ko":"4-4","zh":"4-4"},"apCost":18,"minClearTime":174000},{"zoneId":"main_8","stageId":"main_08-01","code_i18n":{"en":"R8-1","ja":"R8-1","ko":"R8-1","zh":"R8-1"},"apCost":18,"minClearTime":187000},{"zoneId":"gachabox","stageId":"randomMaterial_1","code_i18n":{"en":"Rhodes Island Supplies","ja":"補給物資・ロドス","ko":"로도스 아일랜드 보급 물자","zh":"罗德岛物资补给"},"apCost":99},{"zoneId":"main_5","stageId":"main_05-09","code_i18n":{"en":"5-9","ja":"5-9","ko":"5-9","zh":"5-9"},"apCost":18,"minClearTime":194000},{"zoneId":"act8mini_zone1","stageId":"act8mini_05","code_i18n":{"en":"VI-5","ja":"VI-5","ko":"VI-5","zh":"VI-5"},"apCost":12,"minClearTime":325000},{"zoneId":"main_3","stageId":"main_03-05","code_i18n":{"en":"3-5","ja":"3-5","ko":"3-5","zh":"3-5"},"apCost":15,"minClearTime":165000},{"zoneId":"act16side_zone1","stageId":"act16side_04","code_i18n":{"en":"GA-4","ja":"GA-4","ko":"GA-4","zh":"GA-4"},"apCost":12,"minClearTime":172000},{"zoneId":"permanent_sidestory_8_zone1","stageId":"act16d5_10_perm","code_i18n":{"en":"WR-10","ja":"WR-10","ko":"WR-10","zh":"WR-10"},"apCost":18,"minClearTime":251000},{"zoneId":"main_8","stageId":"main_08-06","code_i18n":{"en":"R8-6","ja":"R8-6","ko":"R8-6","zh":"R8-6"},"apCost":18,"minClearTime":193000},{"zoneId":"main_3","stageId":"main_03-03","code_i18n":{"en":"3-3","ja":"3-3","ko":"3-3","zh":"3-3"},"apCost":15,"minClearTime":172700},{"zoneId":"act4d0_zone1","stageId":"act4d0_05","code_i18n":{"en":"SW-EV-5","ja":"SW-EV-5","ko":"SW-EV-5","zh":"SW-EV-5"},"apCost":18,"minClearTime":214000},{"zoneId":"permanent_sidestory_2_zone1","stageId":"a003_f03_perm","code_i18n":{"en":"OF-F3","ja":"OF-F3","ko":"OF-F3","zh":"OF-F3"},"apCost":15,"minClearTime":171000},{"zoneId":"main_3","stageId":"sub_03-3-1","code_i18n":{"en":"S3-6","ja":"S3-6","ko":"S3-6","zh":"S3-6"},"apCost":15,"minClearTime":122000},{"zoneId":"permanent_sidestory_6_zone1","stageId":"act13d5_02_perm","code_i18n":{"en":"MN-2","ja":"MN-2","ko":"MN-2","zh":"MN-2"},"apCost":9,"minClearTime":102000},{"zoneId":"permanent_sidestory_4_zone1","stageId":"act11d0_01_perm","code_i18n":{"en":"TW-1","ja":"TW-1","ko":"TW-1","zh":"TW-1"},"apCost":9,"minClearTime":150000},{"zoneId":"permanent_sidestory_1_zone1","stageId":"a001_01_perm","code_i18n":{"en":"GT-1","ja":"GT-1","ko":"GT-1","zh":"GT-1"},"apCost":9,"minClearTime":139000},{"zoneId":"permanent_sidestory_7_zone1","stageId":"act15d0_07_perm","code_i18n":{"en":"MB-7","ja":"MB-7","ko":"MB-7","zh":"MB-7"},"apCost":18,"minClearTime":165000},{"zoneId":"main_2","stageId":"sub_02-10","code_i18n":{"en":"S2-10","ja":"S2-10","ko":"S2-10","zh":"S2-10"},"apCost":12,"minClearTime":172000},{"zoneId":"main_10_tough","stageId":"tough_10-06","code_i18n":{"en":"10-7","ja":"10-7","ko":"10-7","zh":"10-7"},"apCost":24,"minClearTime":205000},{"zoneId":"main_10","stageId":"main_10-15","code_i18n":{"en":"10-17","ja":"10-17","ko":"10-17","zh":"10-17"},"apCost":24,"minClearTime":283000},{"zoneId":"act10mini_zone1","stageId":"act10mini_07","code_i18n":{"en":"TB-7","ja":"TB-7","ko":"TB-7","zh":"TB-7"},"apCost":21,"minClearTime":247000},{"zoneId":"main_9","stageId":"main_09-03","code_i18n":{"en":"9-4","ja":"9-4","ko":"9-4","zh":"9-4"},"apCost":18,"minClearTime":219000},{"zoneId":"main_5","stageId":"sub_05-1-1","code_i18n":{"en":"S5-1","ja":"S5-1","ko":"S5-1","zh":"S5-1"},"apCost":18,"minClearTime":193000},{"zoneId":"act13d0_zone1","stageId":"act13d0_06","code_i18n":{"en":"FA-6","ja":"FA-6","ko":"FA-6","zh":"FA-6"},"apCost":12,"minClearTime":184000},{"zoneId":"permanent_sub_3_zone1","stageId":"act18d3_08_perm","code_i18n":{"en":"SV-8","ja":"SV-8","ko":"SV-8","zh":"SV-8"},"apCost":18,"minClearTime":188000},{"zoneId":"act13d0_zone1","stageId":"act13d0_05","code_i18n":{"en":"FA-5","ja":"FA-5","ko":"FA-5","zh":"FA-5"},"apCost":12,"minClearTime":232000},{"zoneId":"main_10","stageId":"main_10-05","code_i18n":{"en":"10-6","ja":"10-6","ko":"10-6","zh":"10-6"},"apCost":21,"minClearTime":182000},{"zoneId":"act9sre_zone1","stageId":"act16d5_06_rep","code_i18n":{"en":"WR-6","ja":"WR-6","ko":"WR-6","zh":"WR-6"},"apCost":12,"minClearTime":169000},{"zoneId":"permanent_sidestory_8_zone1","stageId":"act16d5_08_perm","code_i18n":{"en":"WR-8","ja":"WR-8","ko":"WR-8","zh":"WR-8"},"apCost":15,"minClearTime":206000},{"zoneId":"main_8","stageId":"main_08-13","code_i18n":{"en":"R8-11","ja":"R8-11","ko":"R8-11","zh":"R8-11"},"apCost":21,"minClearTime":274000},{"zoneId":"main_3","stageId":"sub_03-3-2","code_i18n":{"en":"S3-7","ja":"S3-7","ko":"S3-7","zh":"S3-7"},"apCost":18,"minClearTime":147000},{"zoneId":"act8sre_zone1","stageId":"act15d0_04_rep","code_i18n":{"en":"MB-4","ja":"MB-4","ko":"MB-4","zh":"MB-4"},"apCost":12,"minClearTime":169000},{"zoneId":"permanent_sidestory_5_zone1","stageId":"act12d0_09_perm","code_i18n":{"en":"RI-9","ja":"RI-9","ko":"RI-9","zh":"RI-9"},"apCost":18,"minClearTime":258000},{"zoneId":"permanent_sidestory_1_zone1","stageId":"a001_05_perm","code_i18n":{"en":"GT-5","ja":"GT-5","ko":"GT-5","zh":"GT-5"},"apCost":15,"minClearTime":166000},{"zoneId":"act6sre_zone1","stageId":"act12d0_04_rep","code_i18n":{"en":"RI-4","ja":"RI-4","ko":"RI-4","zh":"RI-4"},"apCost":12,"minClearTime":169000},{"zoneId":"gachabox","stageId":"randomMaterial_3","code_i18n":{"en":"32-hour Strategic Ration","ja":"32h戦略補給","ko":"32h 전략 보급","zh":"32h战略配给"},"apCost":99},{"zoneId":"act9d0_zone1","stageId":"act9d0_04","code_i18n":{"en":"DM-4","ja":"DM-4","ko":"DM-4","zh":"DM-4"},"apCost":12,"minClearTime":141000},{"zoneId":"main_7","stageId":"main_07-11","code_i18n":{"en":"7-13","ja":"7-13","ko":"7-13","zh":"7-13"},"apCost":18,"minClearTime":183000},{"zoneId":"permanent_sidestory_4_zone1","stageId":"act11d0_08_perm","code_i18n":{"en":"TW-8","ja":"TW-8","ko":"TW-8","zh":"TW-8"},"apCost":18,"minClearTime":252000},{"zoneId":"permanent_sub_1_zone1","stageId":"act9d0_03_perm","code_i18n":{"en":"DM-3","ja":"DM-3","ko":"DM-3","zh":"DM-3"},"apCost":12,"minClearTime":187000},{"zoneId":"main_10_tough","stageId":"tough_10-09","code_i18n":{"en":"10-10","ja":"10-10","ko":"10-10","zh":"10-10"},"apCost":21,"minClearTime":188000},{"zoneId":"main_10_tough","stageId":"tough_10-15","code_i18n":{"en":"10-17","ja":"10-17","ko":"10-17","zh":"10-17"},"apCost":24,"minClearTime":283000},{"zoneId":"main_6","stageId":"main_06-03","code_i18n":{"en":"6-3","ja":"6-3","ko":"6-3","zh":"6-3"},"apCost":18,"minClearTime":172000},{"zoneId":"main_3","stageId":"sub_03-2-2","code_i18n":{"en":"S3-4","ja":"S3-4","ko":"S3-4","zh":"S3-4"},"apCost":15,"minClearTime":136000},{"zoneId":"permanent_sub_2_zone1","stageId":"act18d0_05_perm","code_i18n":{"en":"WD-5","ja":"WD-5","ko":"WD-5","zh":"WD-5"},"apCost":12,"minClearTime":223000},{"zoneId":"permanent_sidestory_4_zone1","stageId":"act11d0_05_perm","code_i18n":{"en":"TW-5","ja":"TW-5","ko":"TW-5","zh":"TW-5"},"apCost":15,"minClearTime":184000},{"zoneId":"main_10","stageId":"main_10-06","code_i18n":{"en":"10-7","ja":"10-7","ko":"10-7","zh":"10-7"},"apCost":24,"minClearTime":205000},{"zoneId":"act10mini_zone1","stageId":"act10mini_03","code_i18n":{"en":"TB-3","ja":"TB-3","ko":"TB-3","zh":"TB-3"},"apCost":12,"minClearTime":181000},{"zoneId":"act10mini_zone1","stageId":"act10mini_05","code_i18n":{"en":"TB-5","ja":"TB-5","ko":"TB-5","zh":"TB-5"},"apCost":15,"minClearTime":184000},{"zoneId":"main_3","stageId":"main_03-02","code_i18n":{"en":"3-2","ja":"3-2","ko":"3-2","zh":"3-2"},"apCost":15,"minClearTime":173000},{"zoneId":"act7sre_zone1","stageId":"act13d5_05_rep","code_i18n":{"en":"MN-5","ja":"MN-5","ko":"MN-5","zh":"MN-5"},"apCost":15,"minClearTime":150000},{"zoneId":"act6d5_zone1","stageId":"act6d5_03","code_i18n":{"en":"AF-3","ja":"AF-3","ko":"AF-3","zh":"AF-3"},"apCost":12,"minClearTime":171500},{"zoneId":"gachabox","stageId":"randomMaterial_6","code_i18n":{"en":"Rhodes Island Supplies III","ja":"補給物資・ロドスⅢ","ko":"로도스 아일랜드 보급 물자 III","zh":"罗德岛物资补给III"},"apCost":99},{"zoneId":"main_2","stageId":"sub_02-07","code_i18n":{"en":"S2-7","ja":"S2-7","ko":"S2-7","zh":"S2-7"},"apCost":12,"minClearTime":166000},{"zoneId":"act7d5_zone1","stageId":"act7d5_03","code_i18n":{"en":"SA-3","ja":"SA-3","ko":"SA-3","zh":"SA-3"},"apCost":12,"minClearTime":193000},{"zoneId":"main_9","stageId":"main_09-12","code_i18n":{"en":"9-14","ja":"9-14","ko":"9-14","zh":"9-14"},"apCost":21,"minClearTime":208000},{"zoneId":"main_10_tough","stageId":"tough_10-05","code_i18n":{"en":"10-6","ja":"10-6","ko":"10-6","zh":"10-6"},"apCost":21,"minClearTime":182000},{"zoneId":"main_4","stageId":"main_04-10","code_i18n":{"en":"4-10","ja":"4-10","ko":"4-10","zh":"4-10"},"apCost":21,"minClearTime":240500},{"zoneId":"act17d0_zone1","stageId":"act17d0_04","code_i18n":{"en":"OD-4","ja":"OD-4","ko":"OD-4","zh":"OD-4"},"apCost":12,"minClearTime":256000},{"zoneId":"main_10","stageId":"main_10-01","code_i18n":{"en":"10-2","ja":"10-2","ko":"10-2","zh":"10-2"},"apCost":21,"minClearTime":170000},{"zoneId":"main_7","stageId":"sub_07-1-1","code_i18n":{"en":"S7-1","ja":"S7-1","ko":"S7-1","zh":"S7-1"},"apCost":18,"minClearTime":141000}],"zones":[{"zoneId":"act10sre_zone1","type":"ACTIVITY","zoneName_i18n":{"en":"A Walk in The Dust - Rerun","ja":"遺塵の道を・復刻","ko":"워크 인 더 더스트","zh":"遗尘漫步・复刻"}},{"zoneId":"act5sre_zone1","type":"ACTIVITY","zoneName_i18n":{"en":"Twilight of Wolumonde - Rerun","ja":"ウォルモンドの薄暮・復刻","ko":"월루몽드의 황혼 재개방","zh":"沃伦姆德的薄暮・复刻"}},{"zoneId":"permanent_sub_1_zone1","type":"ACTIVITY_PERMANENT","zoneName_i18n":{"en":"Darknights Memoir - Intermezzi","ja":"闇夜に生きる・エピソード","ko":"흑야의 회고록","zh":"生于黑夜・插曲"}},{"zoneId":"act13d0_zone1","type":"ACTIVITY","zoneName_i18n":{"en":"Rewinding Breeze","ja":"在りし日の風を求めて","ko":"리와인딩 브리즈","zh":"踏寻往昔之风"}},{"zoneId":"main_7","type":"MAINLINE","zoneName_i18n":{"en":"Episode 7","ja":"第七章","ko":"에피소드 7","zh":"第七章"}},{"zoneId":"act15d0_zone1","type":"ACTIVITY","zoneName_i18n":{"en":"Mansfield Break","ja":"孤島激震","ko":"맨스필드 브레이크","zh":"孤岛风云"}},{"zoneId":"main_9","type":"MAINLINE","zoneName_i18n":{"en":"Episode 9","ja":"第九章","ko":"에피소드 9","zh":"第九章"}},{"zoneId":"act9sre_zone1","type":"ACTIVITY","zoneName_i18n":{"en":"Who Is Real - Rerun","ja":"画中人・復刻","ko":"화중인 재개방","zh":"画中人・复刻"}},{"zoneId":"act7mini_zone1","type":"ACTIVITY","zoneName_i18n":{"en":"Preluding Lights","ja":"灯火序曲","ko":"프렐류딩 라이츠","zh":"灯火序曲"}},{"zoneId":"act18d3_zone1","type":"ACTIVITY","zoneName_i18n":{"en":"Under Tides","ja":"潮汐の下","ko":"언더 타이즈","zh":"覆潮之下"}},{"zoneId":"act17side_zone1","type":"ACTIVITY","zoneName_i18n":{"en":"愚人号","ja":"愚人号","ko":"愚人号","zh":"愚人号"}},{"zoneId":"act10d5_zone1","type":"ACTIVITY","zoneName_i18n":{"en":"Children of Ursus","ja":"ウルサスの子供たち","ko":"우르수스의 아이들","zh":"乌萨斯的孩子们"}},{"zoneId":"main_10_tough","type":"MAINLINE","zoneName_i18n":{"en":"Episode 10 (Tough)","ja":"第十章 (Tough)","ko":"에피소드 (Tough)","zh":"第十章 (磨难)"}},{"zoneId":"main_10","type":"MAINLINE","zoneName_i18n":{"en":"Episode 10 (Normal)","ja":"第十章 (Normal)","ko":"에피소드 (Normal)","zh":"第十章 (标准)"}},{"zoneId":"act8mini_zone1","type":"ACTIVITY","zoneName_i18n":{"en":"Vigilo","ja":"VIGILO -我が眼に映るまま-","ko":"비질로: 내 눈에 비치는 대로","zh":"如我所见"}},{"zoneId":"permanent_sidestory_6_zone1","type":"ACTIVITY_PERMANENT","zoneName_i18n":{"en":"Maria Nearl - Side Story","ja":"マリア・ニアール・サイドストーリー","ko":"마리아 니어","zh":"玛莉娅・临光・别传"}},{"zoneId":"act10mini_zone1","type":"ACTIVITY","zoneName_i18n":{"en":"阴云火花","ja":"阴云火花","ko":"阴云火花","zh":"阴云火花"}},{"zoneId":"act6sre_zone1","type":"ACTIVITY","zoneName_i18n":{"en":"The Great Chief Returns - Rerun","ja":"帰還!密林の長・復刻","ko":"위대한 족장 가비알: 리턴즈 재개방","zh":"密林悍将归来・复刻"}},{"zoneId":"act9mini_zone1","type":"ACTIVITY","zoneName_i18n":{"en":"Pinus Sylvestris","ja":"赤松林","ko":"피누스 실베스트리스","zh":"红松林"}},{"zoneId":"act16d5_zone1","type":"ACTIVITY","zoneName_i18n":{"en":"Who Is Real","ja":"画中人","ko":"화중인","zh":"画中人"}},{"zoneId":"permanent_sidestory_7_zone1","type":"ACTIVITY_PERMANENT","zoneName_i18n":{"en":"Mansfield Break - Side Story","ja":"孤島激震・サイドストーリー","ko":"맨스필드 브레이크","zh":"孤岛风云・别传"}},{"zoneId":"permanent_sidestory_8_zone1","type":"ACTIVITY_PERMANENT","zoneName_i18n":{"en":"Who Is Real - Side Story","ja":"画中人・サイドストーリー","ko":"화중인","zh":"画中人・别传"}},{"zoneId":"act17d5_zone1","type":"ACTIVITY","zoneName_i18n":{"en":"Darknights Memoir - Rerun","ja":"闇夜に生きる・復刻","ko":"흑야의 회고록 재개방","zh":"生于黑夜・复刻"}},{"zoneId":"permanent_sub_2_zone1","type":"ACTIVITY_PERMANENT","zoneName_i18n":{"en":"A Walk in The Dust - Intermezzi","ja":"遺塵の道を・エピソード","ko":"워크 인 더 더스트","zh":"遗尘漫步・插曲"}},{"zoneId":"act15side_zone1","type":"ACTIVITY","zoneName_i18n":{"en":"将进酒","ja":"将进酒","ko":"将进酒","zh":"将进酒"}},{"zoneId":"act3d0_zone1","type":"ACTIVITY","zoneName_i18n":{"en":"Heart of Surging Flame","ja":"青く燃ゆる心","ko":"파란 불꽃의 마음","zh":"火蓝之心"}},{"zoneId":"act15d5_zone1","type":"ACTIVITY","zoneName_i18n":{"en":"Beyond Here","ja":"彼方を望む","ko":"비욘드 히어","zh":"此地之外"}},{"zoneId":"main_5","type":"MAINLINE","zoneName_i18n":{"en":"Episode 5","ja":"第五章","ko":"에피소드 5","zh":"第五章"}},{"zoneId":"permanent_sidestory_4_zone1","type":"ACTIVITY_PERMANENT","zoneName_i18n":{"en":"Twilight of Wolumonde - Side Story","ja":"ウォルモンドの薄暮・サイドストーリー","ko":"월루몽드의 황혼","zh":"沃伦姆德的薄暮・别传"}},{"zoneId":"act7sre_zone1","type":"ACTIVITY","zoneName_i18n":{"en":"Maria Nearl - Rerun","ja":"マリア・ニアール・復刻","ko":"마리아 니어 재개방","zh":"玛莉娅・临光・复刻"}},{"zoneId":"act14side_zone1","type":"ACTIVITY","zoneName_i18n":{"en":"风雪过境","ja":"风雪过境","ko":"风雪过境","zh":"风雪过境"}},{"zoneId":"act13d5_zone1","type":"ACTIVITY","zoneName_i18n":{"en":"Maria Nearl","ja":"マリア・ニアール","ko":"마리아 니어","zh":"玛莉娅・临光"}},{"zoneId":"act13side_zone1","type":"ACTIVITY","zoneName_i18n":{"en":"Near Light","ja":"ニアーライト","ko":"니어 라이트","zh":"长夜临光"}},{"zoneId":"act12d0_zone1","type":"ACTIVITY","zoneName_i18n":{"en":"The Great Chief Returns","ja":"帰還!密林の長","ko":"위대한 족장 가비알: 리턴즈","zh":"密林悍将归来"}},{"zoneId":"act9d0_zone1","type":"ACTIVITY","zoneName_i18n":{"en":"Darknights Memoir","ja":"闇夜に生きる","ko":"흑야의 회고록","zh":"生于黑夜"}},{"zoneId":"act18d0_zone1","type":"ACTIVITY","zoneName_i18n":{"en":"A Walk in The Dust","ja":"遺塵の道を","ko":"워크 인 더 더스트","zh":"遗尘漫步"}},{"zoneId":"act17d0_zone1","type":"ACTIVITY","zoneName_i18n":{"en":"Operation Originium Dust","ja":"オペレーション オリジニウムダスト","ko":"오리지늄 더스트","zh":"源石尘行动"}},{"zoneId":"act16side_zone1","type":"ACTIVITY","zoneName_i18n":{"en":"吾导先路","ja":"吾导先路","ko":"吾导先路","zh":"吾导先路"}},{"zoneId":"permanent_sidestory_2_zone1","type":"ACTIVITY_PERMANENT","zoneName_i18n":{"en":"Heart of Surging Flame - Side Story","ja":"青く燃ゆる心・サイドストーリー","ko":"파란 불꽃의 마음","zh":"火蓝之心・别传"}},{"zoneId":"permanent_sidestory_5_zone1","type":"ACTIVITY_PERMANENT","zoneName_i18n":{"en":"The Great Chief Returns - Side Story","ja":"帰還!密林の長・サイドストーリー","ko":"위대한 족장 가비알: 리턴즈","zh":"密林悍将归来・别传"}},{"zoneId":"main_2","type":"MAINLINE","zoneName_i18n":{"en":"Episode 2","ja":"第二章","ko":"에피소드 2","zh":"第二章"}},{"zoneId":"main_3","type":"MAINLINE","zoneName_i18n":{"en":"Episode 3","ja":"第三章","ko":"에피소드 3","zh":"第三章"}},{"zoneId":"act11d0_zone1","type":"ACTIVITY","zoneName_i18n":{"en":"Twilight of Wolumonde","ja":"ウォルモンドの薄暮","ko":"월루몽드의 황혼","zh":"沃伦姆德的薄暮"}},{"zoneId":"act8sre_zone1","type":"ACTIVITY","zoneName_i18n":{"en":"Mansfield Break - Rerun","ja":"孤島激震・復刻","ko":"맨스필드 브레이크 재개방","zh":"孤岛风云・复刻"}},{"zoneId":"act7d5_zone1","type":"ACTIVITY","zoneName_i18n":{"en":"Stories of Afternoon","ja":"午後の逸話","ko":"오후의 일화","zh":"午间逸话"}},{"zoneId":"permanent_sidestory_1_zone1","type":"ACTIVITY_PERMANENT","zoneName_i18n":{"en":"The Knight \u0026 The Hunters - Side Story","ja":"騎兵と狩人・サイドストーリー","ko":"기병과 사냥꾼","zh":"骑兵与猎人・别传"}},{"zoneId":"act6d5_zone1","type":"ACTIVITY","zoneName_i18n":{"en":"Ancient Forge","ja":"洪炉示歳","ko":"에인션트 포지","zh":"洪炉示岁"}},{"zoneId":"main_8","type":"MAINLINE","zoneName_i18n":{"en":"Episode 8","ja":"第八章","ko":"에피소드 8","zh":"第八章"}},{"zoneId":"main_6","type":"MAINLINE","zoneName_i18n":{"en":"Episode 6","ja":"第六章","ko":"에피소드 6","zh":"第六章"}},{"zoneId":"main_4","type":"MAINLINE","zoneName_i18n":{"en":"Episode 4","ja":"第四章","ko":"에피소드 4","zh":"第四章"}},{"zoneId":"act4d0_zone1","type":"ACTIVITY","zoneName_i18n":{"en":"Operational Intelligence","ja":"戦地の逸話","ko":"전장의 비화","zh":"战地秘闻"}},{"zoneId":"permanent_sub_3_zone1","type":"ACTIVITY_PERMANENT","zoneName_i18n":{"en":"Under Tides - Intermezzi","ja":"潮汐の下・エピソード","ko":"언더 타이즈","zh":"覆潮之下・插曲"}},{"zoneId":"gachabox","type":"GACHABOX","zoneName_i18n":{"en":"Supplies","ja":"補給物資","ko":"보급물자","zh":"物资补给"}},{"zoneId":"main_1","type":"MAINLINE","zoneName_i18n":{"en":"Episode 1","ja":"第一章","ko":"에피소드 1","zh":"第一章"}}]} 30 | /* # by StageID # */ // const raw = { query: { stageId: 'main_01-07' }, cache: { item: { updated: '2020-11-28T17:48:05.574587+08:00' }, matrix: { updated: '2020-11-28T17:48:06.578839+08:00' }, stage: { updated: '2020-11-28T17:48:09.924366+08:00' }, zone: { updated: '2020-11-28T17:48:10.460206+08:00' } }, items: [{ itemId: 'randomMaterial_4', name_i18n: { en: '感谢庆典物资补给', ja: '感谢庆典物资补给', ko: '感谢庆典物资补给', zh: '感谢庆典物资补给' }, spriteCoord: [4, 13] }, { itemId: 'furni', name_i18n: { en: 'Furniture', ja: '家具', ko: '가구', zh: '家具' } }, { itemId: '30012', name_i18n: { en: 'Orirock Cube', ja: '初級源岩', ko: '원암 큐브', zh: '固源岩' }, spriteCoord: [5, 0] }, { itemId: '3003', name_i18n: { en: 'Pure Gold', ja: '純金', ko: '순금', zh: '赤金' }, spriteCoord: [0, 2] }, { itemId: '30021', name_i18n: { en: 'Sugar Substitute', ja: 'ブドウ糖', ko: '대체당', zh: '代糖' }, spriteCoord: [2, 1] }, { itemId: 'ap_supply_lt_010', name_i18n: { en: 'Emergency Sanity Sampler', ja: '試供理性回復剤', ko: '응급 이성 샘플', zh: '应急理智小样' }, spriteCoord: [0, 9] }, { itemId: 'randomMaterial_2', name_i18n: { en: "New Year's Lantern", ja: '年関ランタン', ko: '축제 등불', zh: '岁过华灯' }, spriteCoord: [1, 9] }, { itemId: '30061', name_i18n: { en: 'Damaged Device', ja: '破損装置', ko: '파손된 장치', zh: '破损装置' }, spriteCoord: [1, 4] }, { itemId: '30051', name_i18n: { en: 'Diketon', ja: 'アケトン試剤', ko: '디케톤', zh: '双酮' }, spriteCoord: [3, 3] }, { itemId: '30031', name_i18n: { en: 'Ester', ja: 'エステル原料', ko: '에스테르 원료', zh: '酯原料' }, spriteCoord: [1, 2] }, { itemId: '30041', name_i18n: { en: 'Oriron Shard', ja: '異鉄の欠片', ko: '이철 조각', zh: '异铁碎片' }, spriteCoord: [5, 2] }, { itemId: '30011', name_i18n: { en: 'Orirock', ja: '源岩鉱', ko: '원암', zh: '源岩' }, spriteCoord: [4, 0] }, { itemId: 'randomMaterial_3', name_i18n: { en: '32h战略配给', ja: '32h战略配给', ko: '32h战略配给', zh: '32h战略配给' }, spriteCoord: [0, 10] }, { itemId: '2001', name_i18n: { en: 'Drill Battle Record', ja: '入門作戦記録', ko: '기초작전기록', zh: '基础作战记录' }, spriteCoord: [0, 0] }, { itemId: 'randomMaterial_1', name_i18n: { en: 'Rhodes Island Supplies', ja: '補給物資・ロドス', ko: '로도스 아일랜드 보급 물자', zh: '罗德岛物资补给' }, spriteCoord: [5, 8] }], matrix: [{ stageId: 'main_01-07', itemId: 'randomMaterial_3', quantity: 5405, times: 48956, start: 1589529600000, end: 1590696000000 }, { stageId: 'main_01-07', itemId: 'randomMaterial_4', quantity: 3687, times: 33460, start: 1604217600000, end: 1605384000000 }, { stageId: 'main_01-07', itemId: 'randomMaterial_1', quantity: 3699, times: 31437, start: 1577174400000, end: 1578340800000 }, { stageId: 'main_01-07', itemId: 'randomMaterial_2', quantity: 2060, times: 17393, start: 1581105600000, end: 1582315200000 }, { stageId: 'main_01-07', itemId: '2001', quantity: 316784, times: 258008, start: 1556676000000 }, { stageId: 'main_01-07', itemId: 'furni', quantity: 1893, times: 258008, start: 1556676000000 }, { stageId: 'main_01-07', itemId: '30051', quantity: 12106, times: 258008, start: 1556676000000 }, { stageId: 'main_01-07', itemId: '30061', quantity: 8792, times: 258008, start: 1556676000000 }, { stageId: 'main_01-07', itemId: '30012', quantity: 320946, times: 258008, start: 1556676000000 }, { stageId: 'main_01-07', itemId: '30031', quantity: 15228, times: 258008, start: 1556676000000 }, { stageId: 'main_01-07', itemId: '30041', quantity: 11997, times: 258008, start: 1556676000000 }, { stageId: 'main_01-07', itemId: '30011', quantity: 30909, times: 258008, start: 1556676000000 }, { stageId: 'main_01-07', itemId: '3003', quantity: 23073, times: 258008, start: 1556676000000 }, { stageId: 'main_01-07', itemId: '30021', quantity: 14573, times: 258008, start: 1556676000000 }, { stageId: 'main_01-07', itemId: 'ap_supply_lt_010', quantity: 2936, times: 33460, start: 1604217600000, end: 1605384000000 }], stages: [{ zoneId: 'main_1', stageId: 'main_01-07', code_i18n: { en: '1-7', ja: '1-7', ko: '1-7', zh: '1-7' }, apCost: 6, minClearTime: 118000 }], zones: [{ zoneId: 'main_1', type: 'MAINLINE', zoneName_i18n: { en: 'Episode 1', ja: '第一章', ko: '에피소드 1', zh: '第一章' } }] } 31 | /* # by StageID Recruit */ // const raw = {"request":{"mirror":"io"},"query":{"stageId":"recruit","server":"CN"},"cache":{"item":{"updated":"2022-08-25T23:06:05.737323664Z"},"matrix":{"updated":"2022-08-25T23:31:05.858918134Z"},"siteStats":{"updated":"2022-08-25T23:31:06.22161397Z"},"stage":{"updated":"2022-08-25T23:06:06.056820349Z"},"zone":{"updated":"2022-08-25T23:06:06.212444615Z"}},"items":[{"itemId":"recruit_tag_defender","name_i18n":{"en":"Defender","ja":"重装","ko":"디펜더","zh":"重装干员"}},{"itemId":"recruit_tag_robot","name_i18n":{"en":"Robot","ja":"ロボット","ko":"로봇이","zh":"支援机械"}},{"itemId":"recruit_tag_nuker","name_i18n":{"en":"Nuker","ja":"爆発力","ko":"누커","zh":"爆发"}},{"itemId":"recruit_tag_shift","name_i18n":{"en":"Shift","ja":"強制移動","ko":"강제이동","zh":"位移"}},{"itemId":"recruit_tag_supporter","name_i18n":{"en":"Supporter","ja":"補助","ko":"서포터","zh":"辅助干员"}},{"itemId":"recruit_tag_caster","name_i18n":{"en":"Caster","ja":"術師","ko":"캐스터","zh":"术师干员"}},{"itemId":"recruit_tag_debuff","name_i18n":{"en":"Debuff","ja":"弱化","ko":"디버프","zh":"削弱"}},{"itemId":"recruit_tag_slow","name_i18n":{"en":"Slow","ja":"減速","ko":"감속","zh":"减速"}},{"itemId":"recruit_tag_sniper","name_i18n":{"en":"Sniper","ja":"狙撃","ko":"스나이퍼","zh":"狙击干员"}},{"itemId":"recruit_tag_fast_redeploy","name_i18n":{"en":"Fast-Redeploy","ja":"高速再配置","ko":"쾌속부활","zh":"快速复活"}},{"itemId":"recruit_tag_ranged","name_i18n":{"en":"Ranged","ja":"遠距離","ko":"원거리로","zh":"远程位"}},{"itemId":"recruit_tag_healing","name_i18n":{"en":"Healing","ja":"治療","ko":"힐링","zh":"治疗"}},{"itemId":"recruit_tag_guard","name_i18n":{"en":"Guard","ja":"前衛","ko":"가드","zh":"近卫干员"}},{"itemId":"recruit_tag_medic","name_i18n":{"en":"Medic","ja":"医療","ko":"메딕","zh":"医疗干员"}},{"itemId":"recruit_tag_vanguard","name_i18n":{"en":"Vanguard","ja":"先鋒","ko":"뱅가드","zh":"先锋干员"}},{"itemId":"recruit_tag_summon","name_i18n":{"en":"Summon","ja":"召喚","ko":"소환","zh":"召唤"}},{"itemId":"recruit_tag_dp_recovery","name_i18n":{"en":"DP-Recovery","ja":"COST回復","ko":"코스트+","zh":"费用回复"}},{"itemId":"recruit_tag_melee","name_i18n":{"en":"Melee","ja":"近距離","ko":"근거리","zh":"近战位"}},{"itemId":"recruit_tag_senior_operator","name_i18n":{"en":"Senior Operator","ja":"エリート","ko":"특별채용","zh":"资深干员"}},{"itemId":"recruit_tag_specialist","name_i18n":{"en":"Specialist","ja":"特殊","ko":"스페셜리스트","zh":"特种干员"}},{"itemId":"recruit_tag_crowd_control","name_i18n":{"en":"Crowd-Control","ja":"牽制","ko":"제어형","zh":"控场"}},{"itemId":"recruit_tag_starter","name_i18n":{"en":"Starter","ja":"初期","ko":"신입","zh":"新手"}},{"itemId":"recruit_tag_support","name_i18n":{"en":"Support","ja":"支援","ko":"지원","zh":"支援"}},{"itemId":"recruit_tag_aoe","name_i18n":{"en":"AoE","ja":"範囲攻撃","ko":"범위공격","zh":"群攻"}},{"itemId":"recruit_tag_dps","name_i18n":{"en":"DPS","ja":"火力","ko":"딜러","zh":"输出"}},{"itemId":"recruit_tag_survival","name_i18n":{"en":"Survival","ja":"生存","ko":"생존형","zh":"生存"}},{"itemId":"recruit_tag_defense","name_i18n":{"en":"Defense","ja":"防御","ko":"방어형","zh":"防护"}},{"itemId":"recruit_tag_top_operator","name_i18n":{"en":"Top Operator","ja":"上級エリート","ko":"고급특별채용","zh":"高级资深干员"}}],"matrix":[{"stageId":"recruit","itemId":"recruit_tag_defense","quantity":10714,"times":48438,"start":1660593600000,"end":1661467164969},{"stageId":"recruit","itemId":"recruit_tag_specialist","quantity":1444,"times":48438,"start":1660593600000,"end":1661467164969},{"stageId":"recruit","itemId":"recruit_tag_healing","quantity":6046,"times":48438,"start":1660593600000,"end":1661467164969},{"stageId":"recruit","itemId":"recruit_tag_supporter","quantity":16235,"times":48438,"start":1660593600000,"end":1661467164969},{"stageId":"recruit","itemId":"recruit_tag_crowd_control","quantity":142,"times":48438,"start":1660593600000,"end":1661467164969},{"stageId":"recruit","itemId":"recruit_tag_nuker","quantity":1535,"times":48438,"start":1660593600000,"end":1661467164969},{"stageId":"recruit","itemId":"recruit_tag_ranged","quantity":16601,"times":48438,"start":1660593600000,"end":1661467164969},{"stageId":"recruit","itemId":"recruit_tag_starter","quantity":18735,"times":48438,"start":1660593600000,"end":1661467164969},{"stageId":"recruit","itemId":"recruit_tag_defender","quantity":13849,"times":48438,"start":1660593600000,"end":1661467164969},{"stageId":"recruit","itemId":"recruit_tag_robot","quantity":655,"times":48438,"start":1660593600000,"end":1661467164969},{"stageId":"recruit","itemId":"recruit_tag_support","quantity":1472,"times":48438,"start":1660593600000,"end":1661467164969},{"stageId":"recruit","itemId":"recruit_tag_shift","quantity":1468,"times":48438,"start":1660593600000,"end":1661467164969},{"stageId":"recruit","itemId":"recruit_tag_aoe","quantity":17458,"times":48438,"start":1660593600000,"end":1661467164969},{"stageId":"recruit","itemId":"recruit_tag_fast_redeploy","quantity":2489,"times":48438,"start":1660593600000,"end":1661467164969},{"stageId":"recruit","itemId":"recruit_tag_dps","quantity":6764,"times":48438,"start":1660593600000,"end":1661467164969},{"stageId":"recruit","itemId":"recruit_tag_caster","quantity":15605,"times":48438,"start":1660593600000,"end":1661467164969},{"stageId":"recruit","itemId":"recruit_tag_debuff","quantity":977,"times":48438,"start":1660593600000,"end":1661467164969},{"stageId":"recruit","itemId":"recruit_tag_slow","quantity":3616,"times":48438,"start":1660593600000,"end":1661467164969},{"stageId":"recruit","itemId":"recruit_tag_dp_recovery","quantity":16500,"times":48438,"start":1660593600000,"end":1661467164969},{"stageId":"recruit","itemId":"recruit_tag_melee","quantity":17822,"times":48438,"start":1660593600000,"end":1661467164969},{"stageId":"recruit","itemId":"recruit_tag_summon","quantity":135,"times":48438,"start":1660593600000,"end":1661467164969},{"stageId":"recruit","itemId":"recruit_tag_guard","quantity":14464,"times":48438,"start":1660593600000,"end":1661467164969},{"stageId":"recruit","itemId":"recruit_tag_medic","quantity":18943,"times":48438,"start":1660593600000,"end":1661467164969},{"stageId":"recruit","itemId":"recruit_tag_sniper","quantity":15311,"times":48438,"start":1660593600000,"end":1661467164969},{"stageId":"recruit","itemId":"recruit_tag_vanguard","quantity":16659,"times":48438,"start":1660593600000,"end":1661467164969},{"stageId":"recruit","itemId":"recruit_tag_top_operator","quantity":141,"times":48438,"start":1660593600000,"end":1661467164969},{"stageId":"recruit","itemId":"recruit_tag_survival","quantity":6308,"times":48438,"start":1660593600000,"end":1661467164969},{"stageId":"recruit","itemId":"recruit_tag_senior_operator","quantity":102,"times":48438,"start":1660593600000,"end":1661467164969}],"stages":[{"zoneId":"recruit","stageId":"recruit","code_i18n":{"en":"Recruit","ja":"公開求人","ko":"공개모집","zh":"公开招募"},"apCost":99}],"zones":[{"zoneId":"recruit","type":"RECRUIT","zoneName_i18n":{"en":"Recruit","ja":"公開求人","ko":"공개모집","zh":"公开招募"}}]} 32 | /* # Error # */ // const raw = { error: { type: 'CantMarshal', details: 'malformed parameter `server` provided' } } 33 | let raw 34 | 35 | try { 36 | raw = JSON.parse(document.querySelector('#penguinWidgetData').textContent) 37 | } catch (e) { 38 | return { 39 | errors: [ 40 | { 41 | type: 'Unknown' 42 | } 43 | ] 44 | } 45 | } 46 | 47 | const errors = validate(raw) 48 | if (errors.length > 0) return { errors } 49 | 50 | let type 51 | if (raw.query.stageId) { 52 | if (raw.query.itemId) { 53 | type = 'exact' 54 | } else { 55 | type = 'stage' 56 | } 57 | } else { 58 | type = 'item' 59 | } 60 | return { 61 | type, 62 | ...raw 63 | } 64 | // return JSON.parse(window.document.querySelector("script#widgetData").textContent) 65 | } 66 | 67 | PenguinData.all = function () { 68 | if (!this._cache) this._cache = this.raw() 69 | return this._cache 70 | } 71 | 72 | PenguinData.items = { 73 | byItemId (itemId) { 74 | return PenguinData.all().items 75 | .find(el => el.itemId === itemId) 76 | } 77 | } 78 | 79 | PenguinData.stages = { 80 | byStageId (stageId) { 81 | return PenguinData.all().stages 82 | .find(el => el.stageId === stageId) 83 | } 84 | } 85 | 86 | PenguinData.zones = { 87 | byZoneId (zoneId) { 88 | return PenguinData.all().zones 89 | .find(el => el.zoneId === zoneId) 90 | } 91 | } 92 | 93 | PenguinData.matrix = { 94 | _cache: null, 95 | data () { 96 | if (!this._cache) { 97 | const raw = PenguinData.all() 98 | this._cache = raw.matrix.map(el => { 99 | const stage = PenguinData.stages.byStageId(el.stageId) 100 | const zone = PenguinData.zones.byZoneId(stage.zoneId) 101 | const item = PenguinData.items.byItemId(el.itemId) 102 | const percentage = el.quantity / el.times 103 | return { 104 | ...el, 105 | stage, 106 | zone, 107 | item, 108 | percentage, 109 | percentageText: `${(percentage * 100).toFixed(2)}%`, 110 | apPPR: (stage.apCost / percentage).toFixed(2) 111 | } 112 | }) 113 | } 114 | return { 115 | ...PenguinData._cache, 116 | matrix: this._cache 117 | } 118 | } 119 | } 120 | 121 | PenguinData.meta = function () { 122 | const { errors, type, query } = PenguinData.all() 123 | if (errors) { 124 | return { 125 | title: i18n.t('errors._title'), 126 | url: PenguinData.mirror().site, 127 | error: true 128 | } 129 | } 130 | let item, itemName, stage, stageName 131 | if (query.itemId) { 132 | item = PenguinData.items.byItemId(query.itemId) 133 | itemName = strings.translate(item, 'name') 134 | } 135 | if (query.stageId) { 136 | stage = PenguinData.stages.byStageId(query.stageId) 137 | stageName = strings.translate(stage, 'code') 138 | } 139 | 140 | if (type === 'item') { 141 | return { 142 | item, 143 | title: i18n.t('title.item', { itemName }), 144 | url: PenguinData.mirror().site + '/result/item/' + item.itemId 145 | } 146 | } else if (type === 'stage') { 147 | return { 148 | title: i18n.t('title.stage', { stageName }), 149 | url: PenguinData.mirror().site + '/result/stage/' + stage.zoneId + '/' + stage.stageId 150 | } 151 | } else if (type === 'exact') { 152 | return { 153 | title: i18n.t('title.exact', { itemName, stageName }), 154 | url: PenguinData.mirror().site + '/result/stage/' + stage.zoneId + '/' + stage.stageId 155 | } 156 | } 157 | } 158 | 159 | PenguinData.request = function () { 160 | return PenguinData.all().request || {} 161 | } 162 | 163 | PenguinData.mirror = function () { 164 | const mirror = PenguinData.request().mirror 165 | switch (mirror) { 166 | case 'io': 167 | return { 168 | site: 'https://penguin-stats.io', 169 | cdn: 'https://penguin-stats.s3.amazonaws.com' 170 | } 171 | case 'cn': 172 | default: 173 | return { 174 | site: 'https://penguin-stats.cn', 175 | cdn: 'https://penguin.upyun.galvincdn.com' 176 | } 177 | } 178 | } 179 | 180 | export default PenguinData 181 | -------------------------------------------------------------------------------- /src/utils/strings.js: -------------------------------------------------------------------------------- 1 | import i18n from '@/i18n' 2 | 3 | function getLocaleMessage (object, localeKey, key, language) { 4 | return object[localeKey][language] || object[localeKey][i18n.fallbackLocale] || object[key] || '' 5 | } 6 | 7 | export default { 8 | translate (object, key) { 9 | const locale = i18n.locale 10 | const localeKey = `${key}_i18n` 11 | if (object) { 12 | if (object[localeKey]) { 13 | if (object[localeKey][locale]) { 14 | return getLocaleMessage(object, localeKey, key, locale) 15 | } else { 16 | const languages = locale.split('-') 17 | return getLocaleMessage(object, localeKey, key, languages[0]) 18 | } 19 | } else { 20 | return object[key] || '' 21 | } 22 | } else { 23 | return '' 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/timeFormatter.js: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | import 'dayjs/locale/zh' 3 | import 'dayjs/locale/ja' 4 | import 'dayjs/locale/ko' 5 | 6 | import i18n from '@/i18n' 7 | 8 | const isBetween = require('dayjs/plugin/isBetween') 9 | const duration = require('dayjs/plugin/duration') 10 | dayjs.extend(isBetween) 11 | dayjs.extend(duration) 12 | 13 | const FORMATS = { 14 | MD: 'M.D', 15 | YMD: 'YY.M.D', 16 | HM: 'H:mm', 17 | HMS: 'H:mm:ss' 18 | } 19 | 20 | function needYear (moments) { 21 | for (const index in moments) { 22 | if (index === '0') continue 23 | if (!dayjs().isSame(moments[index], 'year') || !(moments[index].isSame.apply(moments[index], [moments[index - 1], 'year']))) { 24 | return true 25 | } 26 | } 27 | return false 28 | } 29 | 30 | export default { 31 | get dayjs () { 32 | dayjs.locale(i18n.locale) 33 | return dayjs 34 | }, 35 | isOutdated (rangeStart, rangeEnd) { 36 | return dayjs().isBefore(rangeStart) || dayjs().isAfter(rangeEnd) 37 | }, 38 | dates (times, includeTime = true) { 39 | times = times.map(ts => { 40 | return dayjs(ts) 41 | }) 42 | const needsYear = needYear(times) 43 | times = times.map(time => { 44 | if (includeTime) return time.format(`${needsYear ? FORMATS.YMD : FORMATS.MD} ${FORMATS.HM}`) 45 | return time.format(`${needsYear ? FORMATS.YMD : FORMATS.MD}`) 46 | }) 47 | return times 48 | }, 49 | date (date, detectSameYear = false, includeTime = false) { 50 | let template = FORMATS.MD 51 | if (detectSameYear) { 52 | const isSameYear = dayjs(date).isSame(dayjs(), 'year') 53 | template = isSameYear ? FORMATS.MD : FORMATS.YMD 54 | } 55 | if (includeTime) template += ` ${FORMATS.HMS}` 56 | return dayjs(date).format(template) 57 | }, 58 | /** duration: duration in milliseconds; returns: localized string */ 59 | duration (duration, unit = 's') { 60 | if (!duration) return '' 61 | let message = '' 62 | const d = dayjs.duration(duration / 1000, unit) 63 | if (d.get('minutes') > 0) message += i18n.t('meta.time.minute', { m: d.get('minutes') }) 64 | const ms = d.get('milliseconds') > 0 ? ((d.get('milliseconds') / 1000).toFixed(3)).toString().slice(1) : '' 65 | if (d.get('seconds') > 0) message += i18n.t('meta.time.second', { s: `${d.get('seconds')}${ms}` }) 66 | return message 67 | }, 68 | startEnd (start, end, selector = false) { 69 | if (start && end) { 70 | return i18n.t('table.timeRange.inBetween', this.dates([start, end], false)) 71 | } else if (start && !end) { 72 | return i18n.t('table.timeRange.toPresent', { date: this.date(start, true) }) 73 | } else if (!start && end) { 74 | return i18n.t('table.timeRange.endsAt', { date: this.date(end, true) }) 75 | } else { 76 | if (selector) return i18n.t('stats.timeRange.notSelected') 77 | return i18n.t('table.timeRange.unknown') 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /tests/e2e/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | 'cypress' 4 | ], 5 | env: { 6 | mocha: true, 7 | 'cypress/globals': true 8 | }, 9 | rules: { 10 | strict: 'off' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/e2e/plugins/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable arrow-body-style */ 2 | // https://docs.cypress.io/guides/guides/plugins-guide.html 3 | 4 | // if you need a custom webpack configuration you can uncomment the following import 5 | // and then use the `file:preprocessor` event 6 | // as explained in the cypress docs 7 | // https://docs.cypress.io/api/plugins/preprocessors-api.html#Examples 8 | 9 | // /* eslint-disable import/no-extraneous-dependencies, global-require */ 10 | // const webpack = require('@cypress/webpack-preprocessor') 11 | 12 | module.exports = (on, config) => { 13 | // on('file:preprocessor', webpack({ 14 | // webpackOptions: require('@vue/cli-service/webpack.config'), 15 | // watchOptions: {} 16 | // })) 17 | 18 | return Object.assign({}, config, { 19 | fixturesFolder: 'tests/e2e/fixtures', 20 | integrationFolder: 'tests/e2e/specs', 21 | screenshotsFolder: 'tests/e2e/screenshots', 22 | videosFolder: 'tests/e2e/videos', 23 | supportFile: 'tests/e2e/support/index.js' 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /tests/e2e/specs/test.js: -------------------------------------------------------------------------------- 1 | // https://docs.cypress.io/api/introduction/api.html 2 | 3 | describe('basic functionality', () => { 4 | it('received a document', () => { 5 | cy.visit('/') 6 | }) 7 | it('contains basic element `header`', () => { 8 | cy.get('[penguin\\:element="header"]') 9 | }) 10 | it('contains basic element `content`', () => { 11 | cy.get('[penguin\\:element="content"]') 12 | }) 13 | it('contains basic element `footer`', () => { 14 | cy.get('[penguin\\:element="footer"]') 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /tests/e2e/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This is will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /tests/e2e/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | 3 | require('events').EventEmitter.defaultMaxListeners = 50 4 | 5 | function defineClear (content) { 6 | return JSON.stringify(content.replace(/\n/g, '')).trim() 7 | } 8 | 9 | function cmdOutput (cmd) { 10 | let result 11 | try { 12 | result = require('child_process') 13 | .execSync(cmd) 14 | .toString() 15 | } catch (e) { 16 | result = 'unknown' 17 | } 18 | return defineClear(result) || 'unknown' 19 | } 20 | 21 | module.exports = { 22 | lintOnSave: false, 23 | assetsDir: '_widget', 24 | 25 | pluginOptions: { 26 | i18n: { 27 | locale: 'en', 28 | fallbackLocale: 'en', 29 | localeDir: 'locales', 30 | enableInSFC: false 31 | } 32 | }, 33 | transpileDependencies: [ 34 | 'vuetify' 35 | ], 36 | configureWebpack: { 37 | plugins: [ 38 | new webpack.DefinePlugin({ 39 | NPM_PACKAGE_VERSION: defineClear(process.env.npm_package_version) || 'unknown', 40 | GIT_COMMIT: cmdOutput('git rev-parse --short HEAD'), 41 | BUILD_TIME: cmdOutput('date +%s') 42 | }) 43 | ], 44 | module: { 45 | rules: [ 46 | { 47 | test: /\.ya?ml$/, 48 | use: 'js-yaml-loader' 49 | } 50 | ] 51 | } 52 | } 53 | } 54 | --------------------------------------------------------------------------------