poi-plugin-inventory-cleaner 1.9.2 → 1.11.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 +8 -0
- package/i18n/zh-CN.json +8 -0
- package/index.js +74 -9
- package/lib/equip-types.js +181 -0
- package/lib/inventory.js +49 -1
- package/lib/style.js +18 -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.
|
|
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
|
@@ -30,6 +30,14 @@
|
|
|
30
30
|
"stock.keepMinus": "Keep one less",
|
|
31
31
|
"stock.keepPlus": "Keep one more",
|
|
32
32
|
"stock.batchToggle": "Batch keep",
|
|
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",
|
|
33
41
|
"stock.batchHint": "Set one keep count per category; leave empty and Apply to clear it",
|
|
34
42
|
"stock.batchApply": "Apply",
|
|
35
43
|
"stock.batchKinds": "{{count}} kinds",
|
package/i18n/zh-CN.json
CHANGED
|
@@ -30,6 +30,14 @@
|
|
|
30
30
|
"stock.keepMinus": "少留一件",
|
|
31
31
|
"stock.keepPlus": "多留一件",
|
|
32
32
|
"stock.batchToggle": "批量保留",
|
|
33
|
+
"stock.categoryAll": "全部类别",
|
|
34
|
+
"stock.categoryTitle": "按游戏装备类型筛选",
|
|
35
|
+
"stock.sortTitle": "排序方式",
|
|
36
|
+
"stock.sortVerdict": "按攻略判定",
|
|
37
|
+
"stock.sortCount": "按数量",
|
|
38
|
+
"stock.sortClearable": "按可拆件数",
|
|
39
|
+
"stock.sortName": "按名称",
|
|
40
|
+
"stock.sortCategory": "按类型",
|
|
33
41
|
"stock.batchHint": "按类别统一设置;留空点「应用」= 清除该类自定义值",
|
|
34
42
|
"stock.batchApply": "应用",
|
|
35
43
|
"stock.batchKinds": "{{count}} 种",
|
package/index.js
CHANGED
|
@@ -8,7 +8,9 @@ const {
|
|
|
8
8
|
equipName,
|
|
9
9
|
filterRows,
|
|
10
10
|
stepKeep,
|
|
11
|
+
sortRows,
|
|
11
12
|
summarize,
|
|
13
|
+
OTHER_CATEGORY,
|
|
12
14
|
} = require('./lib/inventory')
|
|
13
15
|
const { buildQuestNeeds } = require('./lib/quest-needs')
|
|
14
16
|
const { CSS } = require('./lib/style')
|
|
@@ -68,14 +70,19 @@ function getState() {
|
|
|
68
70
|
|
|
69
71
|
let snapshot = null
|
|
70
72
|
let snapshotEquips = null
|
|
73
|
+
let snapshotConst = null
|
|
71
74
|
|
|
72
75
|
function getSnapshot() {
|
|
73
76
|
const state = getState()
|
|
74
77
|
const equips =
|
|
75
78
|
state && state.info && state.info.equips ? state.info.equips : null
|
|
76
|
-
|
|
79
|
+
const constant = state ? state.const : null
|
|
80
|
+
if (snapshot && snapshotEquips === equips && snapshotConst === constant) {
|
|
81
|
+
return snapshot
|
|
82
|
+
}
|
|
77
83
|
snapshotEquips = equips
|
|
78
|
-
|
|
84
|
+
snapshotConst = constant
|
|
85
|
+
snapshot = state ? { info: { equips }, const: constant } : null
|
|
79
86
|
return snapshot
|
|
80
87
|
}
|
|
81
88
|
|
|
@@ -134,9 +141,9 @@ function StockRow({ row, onKeepChange }) {
|
|
|
134
141
|
h(
|
|
135
142
|
'div',
|
|
136
143
|
{ className: 'stock-row-main' },
|
|
137
|
-
h('span', { className: 'stock-name' }, row.
|
|
138
|
-
row.cn && row.cn !== row.jp
|
|
139
|
-
? h('span', { className: 'stock-cn' }, row.
|
|
144
|
+
h('span', { className: 'stock-name' }, row.cn || row.jp || `#${row.id}`),
|
|
145
|
+
row.cn && row.jp && row.cn !== row.jp
|
|
146
|
+
? h('span', { className: 'stock-cn' }, row.jp)
|
|
140
147
|
: null,
|
|
141
148
|
h('span', { className: `stock-badge ${verdict.className}` }, t(verdict.labelKey)),
|
|
142
149
|
),
|
|
@@ -260,6 +267,8 @@ function App() {
|
|
|
260
267
|
const state = useExternalStore(subscribe, getSnapshot)
|
|
261
268
|
const [view, setView] = React.useState('all')
|
|
262
269
|
const [keyword, setKeyword] = React.useState('')
|
|
270
|
+
const [category, setCategory] = React.useState('')
|
|
271
|
+
const [sort, setSort] = React.useState('verdict')
|
|
263
272
|
const [keeps, setKeeps] = React.useState(loadKeeps)
|
|
264
273
|
|
|
265
274
|
const handleKeepChange = React.useCallback((id, rawValue) => {
|
|
@@ -295,19 +304,38 @@ function App() {
|
|
|
295
304
|
const map = new Map()
|
|
296
305
|
for (const row of rows) {
|
|
297
306
|
const key = row.category || ''
|
|
298
|
-
const entry = map.get(key) || {
|
|
307
|
+
const entry = map.get(key) || {
|
|
308
|
+
key,
|
|
309
|
+
kinds: 0,
|
|
310
|
+
group: row.group || '',
|
|
311
|
+
groupId: row.groupId || 99,
|
|
312
|
+
categoryId: row.categoryId || 0,
|
|
313
|
+
}
|
|
299
314
|
entry.kinds += 1
|
|
300
315
|
map.set(key, entry)
|
|
301
316
|
}
|
|
302
|
-
return [...map.values()].sort(
|
|
317
|
+
return [...map.values()].sort(
|
|
318
|
+
(a, b) => a.groupId - b.groupId || a.categoryId - b.categoryId,
|
|
319
|
+
)
|
|
303
320
|
}, [rows])
|
|
304
321
|
|
|
322
|
+
const categoryGroups = React.useMemo(() => {
|
|
323
|
+
const map = new Map()
|
|
324
|
+
for (const entry of categories) {
|
|
325
|
+
const name = entry.group || t('stock.batchOther')
|
|
326
|
+
const list = map.get(name) || []
|
|
327
|
+
list.push(entry)
|
|
328
|
+
map.set(name, list)
|
|
329
|
+
}
|
|
330
|
+
return [...map.entries()]
|
|
331
|
+
}, [categories])
|
|
332
|
+
|
|
305
333
|
const applyBatch = (category) => {
|
|
306
334
|
setKeeps((prev) => applyCategoryKeep(prev, rows, category, batchValues[category]))
|
|
307
335
|
}
|
|
308
336
|
const visible = React.useMemo(
|
|
309
|
-
() => filterRows(rows, { view, keyword }),
|
|
310
|
-
[rows, view, keyword],
|
|
337
|
+
() => sortRows(filterRows(rows, { view, keyword, category }), sort),
|
|
338
|
+
[rows, view, keyword, category, sort],
|
|
311
339
|
)
|
|
312
340
|
|
|
313
341
|
const data = React.useMemo(
|
|
@@ -363,6 +391,43 @@ function App() {
|
|
|
363
391
|
placeholder: t('stock.search'),
|
|
364
392
|
onChange: (event) => setKeyword(event.target.value),
|
|
365
393
|
}),
|
|
394
|
+
h(
|
|
395
|
+
'select',
|
|
396
|
+
{
|
|
397
|
+
className: 'stock-category',
|
|
398
|
+
value: category,
|
|
399
|
+
title: t('stock.categoryTitle'),
|
|
400
|
+
onChange: (event) => setCategory(event.target.value),
|
|
401
|
+
},
|
|
402
|
+
h('option', { value: '' }, t('stock.categoryAll')),
|
|
403
|
+
categoryGroups.map(([groupName, list]) =>
|
|
404
|
+
h(
|
|
405
|
+
'optgroup',
|
|
406
|
+
{ key: groupName, label: groupName },
|
|
407
|
+
list.map((entry) =>
|
|
408
|
+
h(
|
|
409
|
+
'option',
|
|
410
|
+
{ key: entry.key || 'other', value: entry.key || OTHER_CATEGORY },
|
|
411
|
+
(entry.key || t('stock.batchOther')) + ' (' + entry.kinds + ')',
|
|
412
|
+
),
|
|
413
|
+
),
|
|
414
|
+
),
|
|
415
|
+
),
|
|
416
|
+
),
|
|
417
|
+
h(
|
|
418
|
+
'select',
|
|
419
|
+
{
|
|
420
|
+
className: 'stock-category stock-sort',
|
|
421
|
+
value: sort,
|
|
422
|
+
title: t('stock.sortTitle'),
|
|
423
|
+
onChange: (event) => setSort(event.target.value),
|
|
424
|
+
},
|
|
425
|
+
h('option', { value: 'verdict' }, t('stock.sortVerdict')),
|
|
426
|
+
h('option', { value: 'count' }, t('stock.sortCount')),
|
|
427
|
+
h('option', { value: 'clearable' }, t('stock.sortClearable')),
|
|
428
|
+
h('option', { value: 'name' }, t('stock.sortName')),
|
|
429
|
+
h('option', { value: 'category' }, t('stock.sortCategory')),
|
|
430
|
+
),
|
|
366
431
|
h(
|
|
367
432
|
'div',
|
|
368
433
|
{ className: 'stock-views' },
|
|
@@ -0,0 +1,181 @@
|
|
|
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
|
+
detailId: detail ? detail.id : 0,
|
|
166
|
+
groupId: detail ? detail.group : 99,
|
|
167
|
+
category: labels ? labels.cn : '',
|
|
168
|
+
categoryJp: labels ? labels.jp : '',
|
|
169
|
+
group: group ? group.cn : '',
|
|
170
|
+
groupJp: group ? group.jp : '',
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
return index
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = {
|
|
177
|
+
GROUPS,
|
|
178
|
+
DETAILS,
|
|
179
|
+
buildTypeIndex,
|
|
180
|
+
filterTypeOf,
|
|
181
|
+
}
|
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,11 @@ 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
|
+
categoryId: typeInfo ? typeInfo.detailId : 0,
|
|
114
|
+
group: typeInfo ? typeInfo.group : '',
|
|
115
|
+
groupId: typeInfo ? typeInfo.groupId : 99,
|
|
116
|
+
guideCategory: rule ? rule.category : '',
|
|
110
117
|
usage: rule ? rule.usage : '',
|
|
111
118
|
obtain: rule ? rule.obtain : '',
|
|
112
119
|
consumed,
|
|
@@ -161,6 +168,36 @@ function stepKeep(row, delta) {
|
|
|
161
168
|
return Math.max(0, Math.min(999, base + delta))
|
|
162
169
|
}
|
|
163
170
|
|
|
171
|
+
const SORTS = ['verdict', 'count', 'clearable', 'name', 'category']
|
|
172
|
+
|
|
173
|
+
function sortRows(rows, sort = 'verdict') {
|
|
174
|
+
const key = SORTS.indexOf(sort) >= 0 ? sort : 'verdict'
|
|
175
|
+
const copy = rows.slice()
|
|
176
|
+
copy.sort((a, b) => {
|
|
177
|
+
if (key === 'count') return b.count - a.count || a.id - b.id
|
|
178
|
+
if (key === 'clearable') return b.clearable - a.clearable || b.count - a.count
|
|
179
|
+
if (key === 'name') {
|
|
180
|
+
const left = String(a.cn || a.jp || '')
|
|
181
|
+
const right = String(b.cn || b.jp || '')
|
|
182
|
+
return left.localeCompare(right, 'zh-CN') || a.id - b.id
|
|
183
|
+
}
|
|
184
|
+
if (key === 'category') {
|
|
185
|
+
return (
|
|
186
|
+
a.groupId - b.groupId ||
|
|
187
|
+
a.categoryId - b.categoryId ||
|
|
188
|
+
b.count - a.count ||
|
|
189
|
+
a.id - b.id
|
|
190
|
+
)
|
|
191
|
+
}
|
|
192
|
+
return (
|
|
193
|
+
VERDICT_RANK[a.verdict] - VERDICT_RANK[b.verdict] ||
|
|
194
|
+
b.count - a.count ||
|
|
195
|
+
a.id - b.id
|
|
196
|
+
)
|
|
197
|
+
})
|
|
198
|
+
return copy
|
|
199
|
+
}
|
|
200
|
+
|
|
164
201
|
function summarize(rows) {
|
|
165
202
|
const totalPieces = rows.reduce((sum, row) => sum + row.count, 0)
|
|
166
203
|
const scrapPieces = rows.reduce((sum, row) => sum + row.clearable, 0)
|
|
@@ -179,12 +216,20 @@ function summarize(rows) {
|
|
|
179
216
|
}
|
|
180
217
|
}
|
|
181
218
|
|
|
219
|
+
const OTHER_CATEGORY = '__other__'
|
|
220
|
+
|
|
182
221
|
function filterRows(rows, options = {}) {
|
|
183
222
|
const keyword = String(options.keyword || '')
|
|
184
223
|
.trim()
|
|
185
224
|
.toLowerCase()
|
|
186
225
|
const view = options.view || 'all'
|
|
226
|
+
const category = String(options.category || '').trim()
|
|
187
227
|
return rows.filter((row) => {
|
|
228
|
+
if (category === OTHER_CATEGORY) {
|
|
229
|
+
if (row.category) return false
|
|
230
|
+
} else if (category && (row.category || '') !== category) {
|
|
231
|
+
return false
|
|
232
|
+
}
|
|
188
233
|
if (view === 'scrap' && row.verdict !== 'scrap' && !row.manual) return false
|
|
189
234
|
if (view === 'protected' && !row.protected) return false
|
|
190
235
|
if (view === 'keepSome' && row.verdict !== 'keepSome') return false
|
|
@@ -202,6 +247,9 @@ function filterRows(rows, options = {}) {
|
|
|
202
247
|
}
|
|
203
248
|
|
|
204
249
|
module.exports = {
|
|
250
|
+
OTHER_CATEGORY,
|
|
251
|
+
SORTS,
|
|
252
|
+
sortRows,
|
|
205
253
|
applyCategoryKeep,
|
|
206
254
|
stepKeep,
|
|
207
255
|
buildInventory,
|
package/lib/style.js
CHANGED
|
@@ -100,6 +100,24 @@ const CSS = `
|
|
|
100
100
|
border-color: var(--stock-indigo);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
.stock-category {
|
|
104
|
+
height: 32px;
|
|
105
|
+
padding: 0 8px;
|
|
106
|
+
border: 1px solid var(--stock-border);
|
|
107
|
+
border-radius: 8px;
|
|
108
|
+
color: inherit;
|
|
109
|
+
background: var(--stock-surface);
|
|
110
|
+
font-family: inherit;
|
|
111
|
+
font-size: 12px;
|
|
112
|
+
cursor: pointer;
|
|
113
|
+
outline: none;
|
|
114
|
+
max-width: 220px;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.stock-category:focus {
|
|
118
|
+
border-color: var(--stock-indigo);
|
|
119
|
+
}
|
|
120
|
+
|
|
103
121
|
.stock-views {
|
|
104
122
|
display: flex;
|
|
105
123
|
gap: 6px;
|