poi-plugin-inventory-cleaner 1.10.0 → 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 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
@@ -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
- if (snapshot && snapshotEquips === equips) return snapshot
79
+ const constant = state ? state.const : null
80
+ if (snapshot && snapshotEquips === equips && snapshotConst === constant) {
81
+ return snapshot
82
+ }
78
83
  snapshotEquips = equips
79
- snapshot = state ? { info: { equips } } : null
84
+ snapshotConst = constant
85
+ snapshot = state ? { info: { equips }, const: constant } : null
80
86
  return snapshot
81
87
  }
82
88
 
@@ -135,9 +141,9 @@ function StockRow({ row, onKeepChange }) {
135
141
  h(
136
142
  'div',
137
143
  { className: 'stock-row-main' },
138
- h('span', { className: 'stock-name' }, row.jp || row.cn || `#${row.id}`),
139
- row.cn && row.cn !== row.jp
140
- ? h('span', { className: 'stock-cn' }, row.cn)
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)
141
147
  : null,
142
148
  h('span', { className: `stock-badge ${verdict.className}` }, t(verdict.labelKey)),
143
149
  ),
@@ -262,6 +268,7 @@ function App() {
262
268
  const [view, setView] = React.useState('all')
263
269
  const [keyword, setKeyword] = React.useState('')
264
270
  const [category, setCategory] = React.useState('')
271
+ const [sort, setSort] = React.useState('verdict')
265
272
  const [keeps, setKeeps] = React.useState(loadKeeps)
266
273
 
267
274
  const handleKeepChange = React.useCallback((id, rawValue) => {
@@ -297,19 +304,38 @@ function App() {
297
304
  const map = new Map()
298
305
  for (const row of rows) {
299
306
  const key = row.category || ''
300
- const entry = map.get(key) || { key, kinds: 0 }
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
+ }
301
314
  entry.kinds += 1
302
315
  map.set(key, entry)
303
316
  }
304
- return [...map.values()].sort((a, b) => b.kinds - a.kinds)
317
+ return [...map.values()].sort(
318
+ (a, b) => a.groupId - b.groupId || a.categoryId - b.categoryId,
319
+ )
305
320
  }, [rows])
306
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
+
307
333
  const applyBatch = (category) => {
308
334
  setKeeps((prev) => applyCategoryKeep(prev, rows, category, batchValues[category]))
309
335
  }
310
336
  const visible = React.useMemo(
311
- () => filterRows(rows, { view, keyword, category }),
312
- [rows, view, keyword, category],
337
+ () => sortRows(filterRows(rows, { view, keyword, category }), sort),
338
+ [rows, view, keyword, category, sort],
313
339
  )
314
340
 
315
341
  const data = React.useMemo(
@@ -370,17 +396,38 @@ function App() {
370
396
  {
371
397
  className: 'stock-category',
372
398
  value: category,
399
+ title: t('stock.categoryTitle'),
373
400
  onChange: (event) => setCategory(event.target.value),
374
401
  },
375
402
  h('option', { value: '' }, t('stock.categoryAll')),
376
- categories.map((entry) =>
403
+ categoryGroups.map(([groupName, list]) =>
377
404
  h(
378
- 'option',
379
- { key: entry.key || 'other', value: entry.key || OTHER_CATEGORY },
380
- (entry.key || t('stock.batchOther')) + ' (' + entry.kinds + ')',
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
+ ),
381
414
  ),
382
415
  ),
383
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
+ ),
384
431
  h(
385
432
  'div',
386
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)
@@ -211,6 +248,8 @@ function filterRows(rows, options = {}) {
211
248
 
212
249
  module.exports = {
213
250
  OTHER_CATEGORY,
251
+ SORTS,
252
+ sortRows,
214
253
  applyCategoryKeep,
215
254
  stepKeep,
216
255
  buildInventory,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poi-plugin-inventory-cleaner",
3
- "version": "1.10.0",
3
+ "version": "1.11.1",
4
4
  "description": "库存清理大师:对照改修攻略与保留上限,找出囤积过量、可以清理的装备,并标出不能拆的改修材料。",
5
5
  "main": "index.js",
6
6
  "files": [