poi-plugin-inventory-cleaner 1.10.0 → 1.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README-CN.md +3 -0
- package/README.md +3 -0
- package/i18n/en-US.json +7 -0
- package/i18n/zh-CN.json +7 -0
- package/index.js +151 -20
- package/lib/equip-types.js +182 -0
- package/lib/inventory.js +41 -1
- package/lib/style.js +115 -0
- package/package.json +1 -1
package/README-CN.md
CHANGED
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
- **联动改修助手**:读该插件本地状态,任何在改修计划里的装备都标「改修计划中」,**不参与可拆**。
|
|
16
16
|
- **未完成任务需求**:汇总任务文本里提到的装备,解析需要的数量(×2 / 2つ / ★max),按数量从可拆里预留,并在列表上方给出「未完成任务需要的装备」清单。任务来源有四个,自动合并去重:poi 进行中任务 → **任务规划插件**(`poi-plugin-quest-planner`)见过的任务 → **任务信息插件**(`poi-plugin-quest-info-2`)的任务列表 → 「我该怎么做」的未接取缓存。**不依赖某一个插件**,都没有时至少统计进行中任务。
|
|
17
17
|
- **联动当前任务**:任务需要的装备标「任务需要 N 件」,不参与可拆的件数就是预留的那部分。
|
|
18
|
+
- **按装备类型筛选**:沿用游戏本体「装备筛选」的 14 个大类 / 60 多个细分类型(与「装备信息」插件一致,如舰上战斗机、夜间轰炸机、高角炮、大口径主炮(II)),下拉框按大类分组。
|
|
19
|
+
- **排序**:按攻略判定 / 数量 / 可拆件数 / 名称 / 类型,随时切换。
|
|
20
|
+
- **名称显示**:中文名为主,日文原名作为副标题保留;每行和分类下拉都带游戏原版装备图标。
|
|
18
21
|
- **筛选与搜索**:全部 / 可拆 / 留一些 / 不拆 / 看计划 / 受保护,支持按名称或 id 搜索。
|
|
19
22
|
|
|
20
23
|
- **独立窗口**:使用 poi 原生的「在新窗口打开插件」功能(`poiPlugin.windowMode`,默认开启)。点击插件图标时 poi 会自己创建窗口并承载插件,带标题栏、主题、缩放和窗口位置记忆;不需要的话在「设置 → 插件 → 库存清理大师」里关掉即可。
|
package/README.md
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
- **Minus/plus steppers** — nudge the keep count by one; starts from the quest requirement when unset.
|
|
14
14
|
- **Remodel Assistant link** — equipment that appears in any improvement plan is marked "in improvement plan" and never counted as scrap.
|
|
15
15
|
- **Unfinished quest needs** — collects equipment from quest text (in-progress quests, `poi-plugin-quest-planner` observed quests, the `poi-plugin-quest-info-2` quest list, or the I-Tell-You cache — whichever is available), parses the required count, and reserves that many pieces. It does not depend on a single plugin; with none installed it still counts in-progress quests.
|
|
16
|
+
- **Type filter** — the game's own 14 equipment categories and 60+ detail types, matching Equipment Info, grouped in the dropdown.
|
|
17
|
+
- **Sorting** — verdict / count / clearable / name / type.
|
|
18
|
+
- **Names** — Chinese name first, original Japanese shown underneath, with the in-game equipment icon on every row and in the type picker.
|
|
16
19
|
- **Filters & search** — all / scrap / keep some / keep / check plan / protected; search by name or id.
|
|
17
20
|
|
|
18
21
|
- **Native window mode** — ships `poiPlugin.windowMode`, so poi opens the plugin in its own managed window (titlebar, theme, zoom, remembered bounds). Toggle it off in poi settings if you prefer the tab.
|
package/i18n/en-US.json
CHANGED
|
@@ -31,6 +31,13 @@
|
|
|
31
31
|
"stock.keepPlus": "Keep one more",
|
|
32
32
|
"stock.batchToggle": "Batch keep",
|
|
33
33
|
"stock.categoryAll": "All categories",
|
|
34
|
+
"stock.categoryTitle": "Filter by in-game equipment type",
|
|
35
|
+
"stock.sortTitle": "Sort by",
|
|
36
|
+
"stock.sortVerdict": "Guide verdict",
|
|
37
|
+
"stock.sortCount": "Count",
|
|
38
|
+
"stock.sortClearable": "Clearable count",
|
|
39
|
+
"stock.sortName": "Name",
|
|
40
|
+
"stock.sortCategory": "Type",
|
|
34
41
|
"stock.batchHint": "Set one keep count per category; leave empty and Apply to clear it",
|
|
35
42
|
"stock.batchApply": "Apply",
|
|
36
43
|
"stock.batchKinds": "{{count}} kinds",
|
package/i18n/zh-CN.json
CHANGED
|
@@ -31,6 +31,13 @@
|
|
|
31
31
|
"stock.keepPlus": "多留一件",
|
|
32
32
|
"stock.batchToggle": "批量保留",
|
|
33
33
|
"stock.categoryAll": "全部类别",
|
|
34
|
+
"stock.categoryTitle": "按游戏装备类型筛选",
|
|
35
|
+
"stock.sortTitle": "排序方式",
|
|
36
|
+
"stock.sortVerdict": "按攻略判定",
|
|
37
|
+
"stock.sortCount": "按数量",
|
|
38
|
+
"stock.sortClearable": "按可拆件数",
|
|
39
|
+
"stock.sortName": "按名称",
|
|
40
|
+
"stock.sortCategory": "按类型",
|
|
34
41
|
"stock.batchHint": "按类别统一设置;留空点「应用」= 清除该类自定义值",
|
|
35
42
|
"stock.batchApply": "应用",
|
|
36
43
|
"stock.batchKinds": "{{count}} 种",
|
package/index.js
CHANGED
|
@@ -8,6 +8,7 @@ const {
|
|
|
8
8
|
equipName,
|
|
9
9
|
filterRows,
|
|
10
10
|
stepKeep,
|
|
11
|
+
sortRows,
|
|
11
12
|
summarize,
|
|
12
13
|
OTHER_CATEGORY,
|
|
13
14
|
} = require('./lib/inventory')
|
|
@@ -69,14 +70,19 @@ function getState() {
|
|
|
69
70
|
|
|
70
71
|
let snapshot = null
|
|
71
72
|
let snapshotEquips = null
|
|
73
|
+
let snapshotConst = null
|
|
72
74
|
|
|
73
75
|
function getSnapshot() {
|
|
74
76
|
const state = getState()
|
|
75
77
|
const equips =
|
|
76
78
|
state && state.info && state.info.equips ? state.info.equips : null
|
|
77
|
-
|
|
79
|
+
const constant = state ? state.const : null
|
|
80
|
+
if (snapshot && snapshotEquips === equips && snapshotConst === constant) {
|
|
81
|
+
return snapshot
|
|
82
|
+
}
|
|
78
83
|
snapshotEquips = equips
|
|
79
|
-
|
|
84
|
+
snapshotConst = constant
|
|
85
|
+
snapshot = state ? { info: { equips }, const: constant } : null
|
|
80
86
|
return snapshot
|
|
81
87
|
}
|
|
82
88
|
|
|
@@ -127,6 +133,100 @@ function Stat({ label, value, tone }) {
|
|
|
127
133
|
)
|
|
128
134
|
}
|
|
129
135
|
|
|
136
|
+
function fallbackIcon(event) {
|
|
137
|
+
const img = event.currentTarget
|
|
138
|
+
if (img.dataset.fallback) return
|
|
139
|
+
img.dataset.fallback = '1'
|
|
140
|
+
img.src = iconUrl(-1)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function iconUrl(iconType) {
|
|
144
|
+
if (typeof window === 'undefined') return ''
|
|
145
|
+
const root = window.ROOT || window.__ROOT || ''
|
|
146
|
+
if (!root) return ''
|
|
147
|
+
const base = String(root).replace(/\\/g, '/').replace(/^\/+/, '')
|
|
148
|
+
return 'file:///' + base + '/assets/img/slotitem/' + (Number(iconType) || 0) + '.png'
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function CategoryPicker({ value, groups, onChange }) {
|
|
152
|
+
const [open, setOpen] = React.useState(false)
|
|
153
|
+
const boxRef = React.useRef(null)
|
|
154
|
+
|
|
155
|
+
React.useEffect(() => {
|
|
156
|
+
if (!open) return undefined
|
|
157
|
+
const onDocClick = (event) => {
|
|
158
|
+
if (boxRef.current && !boxRef.current.contains(event.target)) setOpen(false)
|
|
159
|
+
}
|
|
160
|
+
document.addEventListener('mousedown', onDocClick)
|
|
161
|
+
return () => document.removeEventListener('mousedown', onDocClick)
|
|
162
|
+
}, [open])
|
|
163
|
+
|
|
164
|
+
return h(
|
|
165
|
+
'div',
|
|
166
|
+
{ className: 'stock-picker', ref: boxRef },
|
|
167
|
+
h(
|
|
168
|
+
'button',
|
|
169
|
+
{
|
|
170
|
+
type: 'button',
|
|
171
|
+
className: 'stock-picker-btn',
|
|
172
|
+
title: t('stock.categoryTitle'),
|
|
173
|
+
onClick: () => setOpen((current) => !current),
|
|
174
|
+
},
|
|
175
|
+
h('span', { className: 'stock-picker-label' }, value || t('stock.categoryAll')),
|
|
176
|
+
h('span', { className: 'stock-picker-caret' }, '▾'),
|
|
177
|
+
),
|
|
178
|
+
open
|
|
179
|
+
? h(
|
|
180
|
+
'div',
|
|
181
|
+
{ className: 'stock-picker-panel' },
|
|
182
|
+
h(
|
|
183
|
+
'button',
|
|
184
|
+
{
|
|
185
|
+
type: 'button',
|
|
186
|
+
className: 'stock-picker-item' + (value ? '' : ' is-active'),
|
|
187
|
+
onClick: () => {
|
|
188
|
+
onChange('')
|
|
189
|
+
setOpen(false)
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
h('span', { className: 'stock-picker-name' }, t('stock.categoryAll')),
|
|
193
|
+
),
|
|
194
|
+
groups.map(([groupName, list]) =>
|
|
195
|
+
h(
|
|
196
|
+
'div',
|
|
197
|
+
{ className: 'stock-picker-group', key: groupName },
|
|
198
|
+
h('div', { className: 'stock-picker-title' }, groupName),
|
|
199
|
+
list.map((entry) => {
|
|
200
|
+
const key = entry.key || OTHER_CATEGORY
|
|
201
|
+
return h(
|
|
202
|
+
'button',
|
|
203
|
+
{
|
|
204
|
+
type: 'button',
|
|
205
|
+
key,
|
|
206
|
+
className: 'stock-picker-item' + (value === key ? ' is-active' : ''),
|
|
207
|
+
onClick: () => {
|
|
208
|
+
onChange(key)
|
|
209
|
+
setOpen(false)
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
h('img', {
|
|
213
|
+
className: 'stock-picker-icon',
|
|
214
|
+
src: iconUrl(entry.icon),
|
|
215
|
+
alt: '',
|
|
216
|
+
draggable: 'false',
|
|
217
|
+
onError: fallbackIcon,
|
|
218
|
+
}),
|
|
219
|
+
h('span', { className: 'stock-picker-name' }, entry.key || t('stock.batchOther')),
|
|
220
|
+
h('span', { className: 'stock-picker-count' }, String(entry.kinds)),
|
|
221
|
+
)
|
|
222
|
+
}),
|
|
223
|
+
),
|
|
224
|
+
),
|
|
225
|
+
)
|
|
226
|
+
: null,
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
|
|
130
230
|
function StockRow({ row, onKeepChange }) {
|
|
131
231
|
const verdict = VERDICT[row.verdict] || VERDICT.unknown
|
|
132
232
|
return h(
|
|
@@ -135,9 +235,16 @@ function StockRow({ row, onKeepChange }) {
|
|
|
135
235
|
h(
|
|
136
236
|
'div',
|
|
137
237
|
{ className: 'stock-row-main' },
|
|
138
|
-
h('
|
|
139
|
-
|
|
140
|
-
|
|
238
|
+
h('img', {
|
|
239
|
+
className: 'stock-icon',
|
|
240
|
+
src: iconUrl(row.icon),
|
|
241
|
+
alt: '',
|
|
242
|
+
draggable: 'false',
|
|
243
|
+
onError: fallbackIcon,
|
|
244
|
+
}),
|
|
245
|
+
h('span', { className: 'stock-name' }, row.cn || row.jp || `#${row.id}`),
|
|
246
|
+
row.cn && row.jp && row.cn !== row.jp
|
|
247
|
+
? h('span', { className: 'stock-cn' }, row.jp)
|
|
141
248
|
: null,
|
|
142
249
|
h('span', { className: `stock-badge ${verdict.className}` }, t(verdict.labelKey)),
|
|
143
250
|
),
|
|
@@ -262,6 +369,7 @@ function App() {
|
|
|
262
369
|
const [view, setView] = React.useState('all')
|
|
263
370
|
const [keyword, setKeyword] = React.useState('')
|
|
264
371
|
const [category, setCategory] = React.useState('')
|
|
372
|
+
const [sort, setSort] = React.useState('verdict')
|
|
265
373
|
const [keeps, setKeeps] = React.useState(loadKeeps)
|
|
266
374
|
|
|
267
375
|
const handleKeepChange = React.useCallback((id, rawValue) => {
|
|
@@ -297,19 +405,39 @@ function App() {
|
|
|
297
405
|
const map = new Map()
|
|
298
406
|
for (const row of rows) {
|
|
299
407
|
const key = row.category || ''
|
|
300
|
-
const entry = map.get(key) || {
|
|
408
|
+
const entry = map.get(key) || {
|
|
409
|
+
key,
|
|
410
|
+
kinds: 0,
|
|
411
|
+
icon: row.icon || 0,
|
|
412
|
+
group: row.group || '',
|
|
413
|
+
groupId: row.groupId || 99,
|
|
414
|
+
categoryId: row.categoryId || 0,
|
|
415
|
+
}
|
|
301
416
|
entry.kinds += 1
|
|
302
417
|
map.set(key, entry)
|
|
303
418
|
}
|
|
304
|
-
return [...map.values()].sort(
|
|
419
|
+
return [...map.values()].sort(
|
|
420
|
+
(a, b) => a.groupId - b.groupId || a.categoryId - b.categoryId,
|
|
421
|
+
)
|
|
305
422
|
}, [rows])
|
|
306
423
|
|
|
424
|
+
const categoryGroups = React.useMemo(() => {
|
|
425
|
+
const map = new Map()
|
|
426
|
+
for (const entry of categories) {
|
|
427
|
+
const name = entry.group || t('stock.batchOther')
|
|
428
|
+
const list = map.get(name) || []
|
|
429
|
+
list.push(entry)
|
|
430
|
+
map.set(name, list)
|
|
431
|
+
}
|
|
432
|
+
return [...map.entries()]
|
|
433
|
+
}, [categories])
|
|
434
|
+
|
|
307
435
|
const applyBatch = (category) => {
|
|
308
436
|
setKeeps((prev) => applyCategoryKeep(prev, rows, category, batchValues[category]))
|
|
309
437
|
}
|
|
310
438
|
const visible = React.useMemo(
|
|
311
|
-
() => filterRows(rows, { view, keyword, category }),
|
|
312
|
-
[rows, view, keyword, category],
|
|
439
|
+
() => sortRows(filterRows(rows, { view, keyword, category }), sort),
|
|
440
|
+
[rows, view, keyword, category, sort],
|
|
313
441
|
)
|
|
314
442
|
|
|
315
443
|
const data = React.useMemo(
|
|
@@ -365,21 +493,24 @@ function App() {
|
|
|
365
493
|
placeholder: t('stock.search'),
|
|
366
494
|
onChange: (event) => setKeyword(event.target.value),
|
|
367
495
|
}),
|
|
496
|
+
h(CategoryPicker, {
|
|
497
|
+
value: category,
|
|
498
|
+
groups: categoryGroups,
|
|
499
|
+
onChange: setCategory,
|
|
500
|
+
}),
|
|
368
501
|
h(
|
|
369
502
|
'select',
|
|
370
503
|
{
|
|
371
|
-
className: 'stock-category',
|
|
372
|
-
value:
|
|
373
|
-
|
|
504
|
+
className: 'stock-category stock-sort',
|
|
505
|
+
value: sort,
|
|
506
|
+
title: t('stock.sortTitle'),
|
|
507
|
+
onChange: (event) => setSort(event.target.value),
|
|
374
508
|
},
|
|
375
|
-
h('option', { value: '' }, t('stock.
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
(entry.key || t('stock.batchOther')) + ' (' + entry.kinds + ')',
|
|
381
|
-
),
|
|
382
|
-
),
|
|
509
|
+
h('option', { value: 'verdict' }, t('stock.sortVerdict')),
|
|
510
|
+
h('option', { value: 'count' }, t('stock.sortCount')),
|
|
511
|
+
h('option', { value: 'clearable' }, t('stock.sortClearable')),
|
|
512
|
+
h('option', { value: 'name' }, t('stock.sortName')),
|
|
513
|
+
h('option', { value: 'category' }, t('stock.sortCategory')),
|
|
383
514
|
),
|
|
384
515
|
h(
|
|
385
516
|
'div',
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 装备分类:照搬游戏本体 main.js 的「装备筛选」两级结构
|
|
5
|
+
* (14 个大类 + 细类),和「装备信息」插件(poi-plugin-item-info)显示一致。
|
|
6
|
+
*
|
|
7
|
+
* 装备的筛选类型由 api_type[2] 决定,但游戏会把「夜间机」「高角炮」等
|
|
8
|
+
* 用图标 api_type[3] 提成独立类型,少数装备还有 id 级覆盖,见下面的规则。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const GROUPS = [
|
|
12
|
+
{ id: 1, cn: '舰载战斗机', jp: '艦上戦闘機' },
|
|
13
|
+
{ id: 2, cn: '舰载轰炸·攻击机', jp: '艦上爆撃・攻撃機' },
|
|
14
|
+
{ id: 3, cn: '侦察机', jp: '偵察機' },
|
|
15
|
+
{ id: 4, cn: '主炮', jp: '主砲' },
|
|
16
|
+
{ id: 5, cn: '弹药', jp: '弾薬' },
|
|
17
|
+
{ id: 6, cn: '副炮·对空', jp: '副砲・対空' },
|
|
18
|
+
{ id: 7, cn: '鱼雷', jp: '魚雷' },
|
|
19
|
+
{ id: 8, cn: '对潜', jp: '対潜' },
|
|
20
|
+
{ id: 9, cn: '电探', jp: '電探' },
|
|
21
|
+
{ id: 10, cn: '登陆·运输', jp: '上陸・輸送' },
|
|
22
|
+
{ id: 11, cn: '人员·物资', jp: '人員・物資' },
|
|
23
|
+
{ id: 12, cn: '陆上机', jp: '陸上機' },
|
|
24
|
+
{ id: 13, cn: '舰体强化', jp: '艦体強化' },
|
|
25
|
+
{ id: 14, cn: '其他', jp: 'その他' },
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
const GROUP_BY_ID = new Map(GROUPS.map((group) => [group.id, group]))
|
|
29
|
+
|
|
30
|
+
/** 细类:id 沿用游戏筛选里的编号,types 是归到这一类的筛选类型。 */
|
|
31
|
+
const DETAILS = [
|
|
32
|
+
{ id: 11, group: 1, types: [6, 56] },
|
|
33
|
+
{ id: 12, group: 1, types: [82] },
|
|
34
|
+
{ id: 21, group: 2, types: [7, 57] },
|
|
35
|
+
{ id: 22, group: 2, types: [8, 58] },
|
|
36
|
+
{ id: 23, group: 2, types: [83] },
|
|
37
|
+
{ id: 24, group: 2, types: [84] },
|
|
38
|
+
{ id: 31, group: 3, types: [10, 85] },
|
|
39
|
+
{ id: 32, group: 3, types: [9, 94] },
|
|
40
|
+
{ id: 33, group: 3, types: [11] },
|
|
41
|
+
{ id: 34, group: 3, types: [26, 45, 59] },
|
|
42
|
+
{ id: 35, group: 3, types: [25] },
|
|
43
|
+
{ id: 41, group: 4, types: [1] },
|
|
44
|
+
{ id: 42, group: 4, types: [2] },
|
|
45
|
+
{ id: 43, group: 4, types: [3, 38] },
|
|
46
|
+
{ id: 51, group: 5, types: [19] },
|
|
47
|
+
{ id: 52, group: 5, types: [18] },
|
|
48
|
+
{ id: 53, group: 5, types: [20, 33] },
|
|
49
|
+
{ id: 61, group: 6, types: [4, 95] },
|
|
50
|
+
{ id: 62, group: 6, types: [81] },
|
|
51
|
+
{ id: 63, group: 6, types: [21] },
|
|
52
|
+
{ id: 64, group: 6, types: [36] },
|
|
53
|
+
{ id: 71, group: 7, types: [5] },
|
|
54
|
+
{ id: 72, group: 7, types: [22] },
|
|
55
|
+
{ id: 73, group: 7, types: [32] },
|
|
56
|
+
{ id: 81, group: 8, types: [14, 40] },
|
|
57
|
+
{ id: 82, group: 8, types: [15] },
|
|
58
|
+
{ id: 91, group: 9, types: [12] },
|
|
59
|
+
{ id: 92, group: 9, types: [13, 93] },
|
|
60
|
+
{ id: 93, group: 9, types: [51] },
|
|
61
|
+
{ id: 101, group: 10, types: [24] },
|
|
62
|
+
{ id: 102, group: 10, types: [46] },
|
|
63
|
+
{ id: 103, group: 10, types: [30, 50, 52] },
|
|
64
|
+
{ id: 111, group: 11, types: [43] },
|
|
65
|
+
{ id: 112, group: 11, types: [23] },
|
|
66
|
+
{ id: 113, group: 11, types: [39] },
|
|
67
|
+
{ id: 114, group: 11, types: [35, 44] },
|
|
68
|
+
{ id: 121, group: 12, types: [47, 91] },
|
|
69
|
+
{ id: 122, group: 12, types: [48] },
|
|
70
|
+
{ id: 123, group: 12, types: [49, 53, 41] },
|
|
71
|
+
{ id: 131, group: 13, types: [17] },
|
|
72
|
+
{ id: 132, group: 13, types: [16, 27, 28] },
|
|
73
|
+
{ id: 141, group: 14, types: [29, 42] },
|
|
74
|
+
{ id: 142, group: 14, types: [37] },
|
|
75
|
+
{ id: 143, group: 14, types: [54] },
|
|
76
|
+
{ id: 144, group: 14, types: [31, 34] },
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
/** 客户端里才有的筛选类型(游戏类型表里没有),自己给名字。 */
|
|
80
|
+
const SYNTHETIC_NAMES = {
|
|
81
|
+
81: { cn: '高角炮', jp: '高角砲' },
|
|
82
|
+
82: { cn: '夜间战斗机', jp: '夜間戦闘機' },
|
|
83
|
+
83: { cn: '夜间轰炸机', jp: '夜間爆撃機' },
|
|
84
|
+
84: { cn: '夜间攻击机', jp: '夜間攻撃機' },
|
|
85
|
+
85: { cn: '夜间侦察机', jp: '夜間偵察機' },
|
|
86
|
+
91: { cn: '喷式轰炸机(II)', jp: '噴式爆撃機(II)' },
|
|
87
|
+
93: { cn: '大型电探(II)', jp: '大型電探(II)' },
|
|
88
|
+
94: { cn: '舰上侦察机(II)', jp: '艦上偵察機(II)' },
|
|
89
|
+
95: { cn: '副炮(II)', jp: '副砲(II)' },
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** id 级覆盖:这些装备在游戏里被单独提出来筛选。 */
|
|
93
|
+
const TYPE_OVERRIDES = {
|
|
94
|
+
128: 38,
|
|
95
|
+
281: 38,
|
|
96
|
+
465: 38,
|
|
97
|
+
142: 93,
|
|
98
|
+
460: 93,
|
|
99
|
+
151: 94,
|
|
100
|
+
561: 91,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** 图标提级:白天/夜间、普通炮/高角炮共用类型号,靠图标区分。 */
|
|
104
|
+
const ICON_RULES = [
|
|
105
|
+
{ types: [1, 2, 3, 4], icon: 16, result: 81 },
|
|
106
|
+
{ types: [6, 56], icon: 45, result: 82 },
|
|
107
|
+
{ types: [7, 57], icon: 58, result: 83 },
|
|
108
|
+
{ types: [8, 58], icon: 46, result: 84 },
|
|
109
|
+
{ types: [10], icon: 50, result: 85 },
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
const DETAIL_BY_TYPE = new Map()
|
|
113
|
+
for (const detail of DETAILS) {
|
|
114
|
+
for (const type of detail.types) DETAIL_BY_TYPE.set(type, detail)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function filterTypeOf(id, apiType) {
|
|
118
|
+
const override = TYPE_OVERRIDES[Number(id)]
|
|
119
|
+
if (override) return override
|
|
120
|
+
if (!Array.isArray(apiType)) return 0
|
|
121
|
+
const type = Number(apiType[2]) || 0
|
|
122
|
+
const icon = Number(apiType[3]) || 0
|
|
123
|
+
for (const rule of ICON_RULES) {
|
|
124
|
+
if (rule.icon === icon && rule.types.indexOf(type) >= 0) return rule.result
|
|
125
|
+
}
|
|
126
|
+
return type
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function labelsFor(filterType, equipTypes, detail) {
|
|
130
|
+
const synthetic = SYNTHETIC_NAMES[filterType]
|
|
131
|
+
if (synthetic) return synthetic
|
|
132
|
+
|
|
133
|
+
const own = equipTypes && equipTypes[filterType]
|
|
134
|
+
if (own && own.api_name) return { cn: String(own.api_name), jp: '' }
|
|
135
|
+
|
|
136
|
+
// 合成类型(如 大口径主炮(II))没有自己的名字,用所属细类的第一个类型的名字
|
|
137
|
+
if (detail) {
|
|
138
|
+
for (const type of detail.types) {
|
|
139
|
+
const base = equipTypes && equipTypes[type]
|
|
140
|
+
if (base && base.api_name) return { cn: String(base.api_name), jp: '' }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return null
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 生成装备 id → 分类信息。数据来自 poi 的主数据:
|
|
148
|
+
* state.const.$equips(api_type)与 state.const.$equipTypes(类型名)。
|
|
149
|
+
*/
|
|
150
|
+
function buildTypeIndex(state) {
|
|
151
|
+
const index = new Map()
|
|
152
|
+
const constant = state && state.const ? state.const : {}
|
|
153
|
+
const equips = constant.$equips
|
|
154
|
+
const equipTypes = constant.$equipTypes
|
|
155
|
+
if (!equips || typeof equips !== 'object') return index
|
|
156
|
+
|
|
157
|
+
for (const [id, master] of Object.entries(equips)) {
|
|
158
|
+
if (!master || typeof master !== 'object') continue
|
|
159
|
+
const filterType = filterTypeOf(id, master.api_type)
|
|
160
|
+
const detail = DETAIL_BY_TYPE.get(filterType)
|
|
161
|
+
const labels = labelsFor(filterType, equipTypes, detail)
|
|
162
|
+
const group = detail ? GROUP_BY_ID.get(detail.group) : null
|
|
163
|
+
index.set(Number(id), {
|
|
164
|
+
filterType,
|
|
165
|
+
icon: Number(master.api_type && master.api_type[3]) || 0,
|
|
166
|
+
detailId: detail ? detail.id : 0,
|
|
167
|
+
groupId: detail ? detail.group : 99,
|
|
168
|
+
category: labels ? labels.cn : '',
|
|
169
|
+
categoryJp: labels ? labels.jp : '',
|
|
170
|
+
group: group ? group.cn : '',
|
|
171
|
+
groupJp: group ? group.jp : '',
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
return index
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = {
|
|
178
|
+
GROUPS,
|
|
179
|
+
DETAILS,
|
|
180
|
+
buildTypeIndex,
|
|
181
|
+
filterTypeOf,
|
|
182
|
+
}
|
package/lib/inventory.js
CHANGED
|
@@ -15,6 +15,7 @@ const CONSUMED = require('../data/consumed-materials.json')
|
|
|
15
15
|
const GUIDE = require('../data/equipment-guide.json')
|
|
16
16
|
const { getProtection } = require('./protect')
|
|
17
17
|
const { buildQuestNeeds } = require('./quest-needs')
|
|
18
|
+
const { buildTypeIndex } = require('./equip-types')
|
|
18
19
|
|
|
19
20
|
function isRecord(value) {
|
|
20
21
|
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
@@ -47,6 +48,7 @@ function buildInventory(state, options = {}) {
|
|
|
47
48
|
const protection = options.protection || getProtection(state)
|
|
48
49
|
const planned = protection.planned || new Set()
|
|
49
50
|
const keeps = options.keeps || {}
|
|
51
|
+
const typeIndex = options.typeIndex || buildTypeIndex(state)
|
|
50
52
|
const questNeeds = options.questNeeds || buildQuestNeeds(state)
|
|
51
53
|
|
|
52
54
|
const byId = new Map()
|
|
@@ -72,6 +74,7 @@ function buildInventory(state, options = {}) {
|
|
|
72
74
|
const guide = GUIDE[String(row.id)] || null
|
|
73
75
|
const consumed = CONSUMED[String(row.id)] || ''
|
|
74
76
|
|
|
77
|
+
const typeInfo = typeIndex.get(row.id) || null
|
|
75
78
|
const verdict = rule ? rule.verdict : 'unknown'
|
|
76
79
|
const inPlan = planned.has(row.id)
|
|
77
80
|
const need = questNeeds.byEquip.get(row.id) || null
|
|
@@ -106,7 +109,12 @@ function buildInventory(state, options = {}) {
|
|
|
106
109
|
unlocked: row.unlocked,
|
|
107
110
|
verdict,
|
|
108
111
|
rawVerdict: rule ? rule.raw : '',
|
|
109
|
-
category: rule ? rule.category : '',
|
|
112
|
+
category: typeInfo && typeInfo.category ? typeInfo.category : (rule ? rule.category : ''),
|
|
113
|
+
icon: typeInfo ? typeInfo.icon : 0,
|
|
114
|
+
categoryId: typeInfo ? typeInfo.detailId : 0,
|
|
115
|
+
group: typeInfo ? typeInfo.group : '',
|
|
116
|
+
groupId: typeInfo ? typeInfo.groupId : 99,
|
|
117
|
+
guideCategory: rule ? rule.category : '',
|
|
110
118
|
usage: rule ? rule.usage : '',
|
|
111
119
|
obtain: rule ? rule.obtain : '',
|
|
112
120
|
consumed,
|
|
@@ -161,6 +169,36 @@ function stepKeep(row, delta) {
|
|
|
161
169
|
return Math.max(0, Math.min(999, base + delta))
|
|
162
170
|
}
|
|
163
171
|
|
|
172
|
+
const SORTS = ['verdict', 'count', 'clearable', 'name', 'category']
|
|
173
|
+
|
|
174
|
+
function sortRows(rows, sort = 'verdict') {
|
|
175
|
+
const key = SORTS.indexOf(sort) >= 0 ? sort : 'verdict'
|
|
176
|
+
const copy = rows.slice()
|
|
177
|
+
copy.sort((a, b) => {
|
|
178
|
+
if (key === 'count') return b.count - a.count || a.id - b.id
|
|
179
|
+
if (key === 'clearable') return b.clearable - a.clearable || b.count - a.count
|
|
180
|
+
if (key === 'name') {
|
|
181
|
+
const left = String(a.cn || a.jp || '')
|
|
182
|
+
const right = String(b.cn || b.jp || '')
|
|
183
|
+
return left.localeCompare(right, 'zh-CN') || a.id - b.id
|
|
184
|
+
}
|
|
185
|
+
if (key === 'category') {
|
|
186
|
+
return (
|
|
187
|
+
a.groupId - b.groupId ||
|
|
188
|
+
a.categoryId - b.categoryId ||
|
|
189
|
+
b.count - a.count ||
|
|
190
|
+
a.id - b.id
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
return (
|
|
194
|
+
VERDICT_RANK[a.verdict] - VERDICT_RANK[b.verdict] ||
|
|
195
|
+
b.count - a.count ||
|
|
196
|
+
a.id - b.id
|
|
197
|
+
)
|
|
198
|
+
})
|
|
199
|
+
return copy
|
|
200
|
+
}
|
|
201
|
+
|
|
164
202
|
function summarize(rows) {
|
|
165
203
|
const totalPieces = rows.reduce((sum, row) => sum + row.count, 0)
|
|
166
204
|
const scrapPieces = rows.reduce((sum, row) => sum + row.clearable, 0)
|
|
@@ -211,6 +249,8 @@ function filterRows(rows, options = {}) {
|
|
|
211
249
|
|
|
212
250
|
module.exports = {
|
|
213
251
|
OTHER_CATEGORY,
|
|
252
|
+
SORTS,
|
|
253
|
+
sortRows,
|
|
214
254
|
applyCategoryKeep,
|
|
215
255
|
stepKeep,
|
|
216
256
|
buildInventory,
|
package/lib/style.js
CHANGED
|
@@ -100,6 +100,121 @@ const CSS = `
|
|
|
100
100
|
border-color: var(--stock-indigo);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
.stock-icon {
|
|
104
|
+
width: 24px;
|
|
105
|
+
height: 24px;
|
|
106
|
+
flex: none;
|
|
107
|
+
border-radius: 4px;
|
|
108
|
+
opacity: 0.92;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.stock-picker {
|
|
112
|
+
position: relative;
|
|
113
|
+
display: inline-block;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.stock-picker-btn {
|
|
117
|
+
display: inline-flex;
|
|
118
|
+
align-items: center;
|
|
119
|
+
gap: 6px;
|
|
120
|
+
height: 32px;
|
|
121
|
+
max-width: 240px;
|
|
122
|
+
padding: 0 10px;
|
|
123
|
+
border: 1px solid var(--stock-border);
|
|
124
|
+
border-radius: 8px;
|
|
125
|
+
color: inherit;
|
|
126
|
+
background: var(--stock-surface);
|
|
127
|
+
font-family: inherit;
|
|
128
|
+
font-size: 12px;
|
|
129
|
+
cursor: pointer;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.stock-picker-btn:hover {
|
|
133
|
+
border-color: var(--stock-border-strong);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.stock-picker-label {
|
|
137
|
+
overflow: hidden;
|
|
138
|
+
text-overflow: ellipsis;
|
|
139
|
+
white-space: nowrap;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.stock-picker-caret {
|
|
143
|
+
flex: none;
|
|
144
|
+
opacity: 0.6;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.stock-picker-panel {
|
|
148
|
+
position: absolute;
|
|
149
|
+
top: calc(100% + 4px);
|
|
150
|
+
left: 0;
|
|
151
|
+
z-index: 60;
|
|
152
|
+
width: 330px;
|
|
153
|
+
max-height: 440px;
|
|
154
|
+
overflow-y: auto;
|
|
155
|
+
padding: 6px;
|
|
156
|
+
border: 1px solid var(--stock-border-strong);
|
|
157
|
+
border-radius: 10px;
|
|
158
|
+
background: #1b2129;
|
|
159
|
+
box-shadow: 0 14px 34px rgba(0, 0, 0, 0.5);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.stock-picker-group + .stock-picker-group {
|
|
163
|
+
margin-top: 2px;
|
|
164
|
+
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.stock-picker-title {
|
|
168
|
+
padding: 8px 8px 3px;
|
|
169
|
+
font-size: 10px;
|
|
170
|
+
letter-spacing: 0.06em;
|
|
171
|
+
color: rgba(232, 237, 244, 0.48);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.stock-picker-item {
|
|
175
|
+
display: flex;
|
|
176
|
+
align-items: center;
|
|
177
|
+
gap: 8px;
|
|
178
|
+
width: 100%;
|
|
179
|
+
padding: 6px 8px;
|
|
180
|
+
border: 0;
|
|
181
|
+
border-radius: 7px;
|
|
182
|
+
color: #e8edf4;
|
|
183
|
+
background: transparent;
|
|
184
|
+
font-family: inherit;
|
|
185
|
+
font-size: 12px;
|
|
186
|
+
text-align: left;
|
|
187
|
+
cursor: pointer;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
.stock-picker-item:hover {
|
|
191
|
+
background: rgba(255, 255, 255, 0.08);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.stock-picker-item.is-active {
|
|
195
|
+
background: rgba(99, 102, 241, 0.4);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.stock-picker-icon {
|
|
199
|
+
width: 22px;
|
|
200
|
+
height: 22px;
|
|
201
|
+
flex: none;
|
|
202
|
+
border-radius: 3px;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.stock-picker-name {
|
|
206
|
+
flex: 1 1 auto;
|
|
207
|
+
overflow: hidden;
|
|
208
|
+
text-overflow: ellipsis;
|
|
209
|
+
white-space: nowrap;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.stock-picker-count {
|
|
213
|
+
flex: none;
|
|
214
|
+
font-variant-numeric: tabular-nums;
|
|
215
|
+
opacity: 0.5;
|
|
216
|
+
}
|
|
217
|
+
|
|
103
218
|
.stock-category {
|
|
104
219
|
height: 32px;
|
|
105
220
|
padding: 0 8px;
|