├── .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 |
4 |
5 | # Penguin Statistics - Widget `frontend`
6 | [](#readme)
7 | [](#readme)
8 | [](#readme)
9 | [](https://github.com/penguin-statistics/widget-frontend/blob/main/LICENSE)
10 | [](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 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
14 |
20 |
21 |
24 |
27 |
28 | {{ $t('app.vendor') }} {{ $t('app.name') }} {{ year }} |
29 |
34 | {{ $t('source') }}
35 |
36 |
37 |
41 | {{ $t('copyright') }}
42 |
43 |
47 | {{ $t('version', version) }}
48 |
49 |
50 |
51 |
52 |
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 |
2 |
--------------------------------------------------------------------------------
/src/components/DataTable.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
28 |
29 |
32 |
--------------------------------------------------------------------------------
/src/components/Header.vue:
--------------------------------------------------------------------------------
1 |
2 |
42 |
43 |
44 |
69 |
70 |
163 |
--------------------------------------------------------------------------------
/src/components/ItemIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
16 | {{ mdiLamp }}
17 |
18 |
25 | {{ mdiTreasureChest }}
26 |
27 |
28 |
29 |
122 |
123 |
141 |
--------------------------------------------------------------------------------
/src/components/MoreDataIndicator.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
14 | {{ mdiChevronDoubleLeft }}
15 |
16 |
17 | {{ $t('table.scroll') }}
18 |
19 |
24 | {{ mdiChevronDoubleRight }}
25 |
26 |
27 |
28 |
29 |
30 |
42 |
43 |
46 |
--------------------------------------------------------------------------------
/src/components/Settings.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
24 |
25 |
30 |
--------------------------------------------------------------------------------
/src/components/StageCode.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
125 |
126 |
138 |
--------------------------------------------------------------------------------
/src/components/ZoneName.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 | {{ zoneType }}
9 |
10 |
11 | {{ zoneName }}
12 |
13 |
14 |
15 |
16 |
38 |
39 |
42 |
--------------------------------------------------------------------------------
/src/components/fact-table/FactTable.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/src/components/fact-table/FactTableItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
11 | {{ title }}
12 |
13 |
14 |
15 |
19 |
23 | {{ content }}
24 |
25 |
26 |
27 |
28 |
29 |
48 |
49 |
--------------------------------------------------------------------------------
/src/components/settings/LocaleSwitcher.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 | {{ mdiTranslate }}
8 |
9 |
10 |
14 |
15 |
23 | {{ locale.text }}
24 |
25 |
26 |
27 |
28 |
70 |
71 |
74 |
--------------------------------------------------------------------------------
/src/components/settings/ThemeSwitcher.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 | {{ mdiInvertColors }}
8 |
9 |
10 |
14 |
15 |
23 | {{ $t('settings.theme.' + theme) }}
24 |
25 |
26 |
27 |
28 |
57 |
58 |
61 |
--------------------------------------------------------------------------------
/src/components/table/DataTableError.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
14 |
18 |
19 | {{ $t('errors._sorry') }}
20 |
21 |
22 | {{ $t('errors._prefix') }}
23 |
24 |
25 |
29 | {{ mdiAlertCircle }}
30 |
31 | {{ $t('errors._reason', {error: errorMessage}) }}
32 |
33 |
34 | {{ $t('errors._reported') }}
35 |
36 |
37 |
38 |
39 |
40 |
41 |
67 |
68 |
71 |
--------------------------------------------------------------------------------
/src/components/table/DataTableItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | —
27 |
28 |
29 | {{ item.stage.apCost }}
30 |
31 |
32 |
33 | {{ item.percentageText }}
34 |
35 |
36 |
37 |
38 |
39 |
99 |
100 |
103 |
--------------------------------------------------------------------------------
/src/components/table/DataTableStage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
18 |
19 |
20 |
21 |
27 |
28 |
29 | {{ item.itemName }}
30 |
31 |
32 |
33 |
34 | {{ item.percentageText }}
35 |
36 |
37 |
38 |
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 |
--------------------------------------------------------------------------------