poi-plugin-lbas-bis 0.1.0

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/index.js ADDED
@@ -0,0 +1,558 @@
1
+ 'use strict';
2
+
3
+ const React = require('react');
4
+ const { optimizeLoadouts } = require('./src/optimizer');
5
+ const { extractOptimizationPlanes, extractOwnedPlanes } = require('./src/poi-data');
6
+ const {
7
+ createEmptySimulatorState,
8
+ normalizeSimulatorState,
9
+ setBaseCount,
10
+ setBaseSlot,
11
+ setSlotLock,
12
+ setWaveTarget,
13
+ simulatorToOptimizerInput,
14
+ } = require('./src/simulator-state');
15
+ const { calculateSimulatorSummary } = require('./src/simulator-calc');
16
+ const { applyPlanToSimulator } = require('./src/import-plan');
17
+ const SimulatorPanel = require('./src/ui/SimulatorPanel');
18
+ const OptimizerPanel = require('./src/ui/OptimizerPanel');
19
+
20
+ const h = React.createElement;
21
+ const PLUGIN_ID = 'lbas_bis';
22
+ const STATE_OPTIONS = ['denial', 'parity', 'superiority', 'supremacy'];
23
+
24
+ const FALLBACK_ZH_CN = {
25
+ title: '陆航优化',
26
+ simulatorTitle: '基地航空队模拟器',
27
+ optimizerTitle: '配装优化',
28
+ targetRadius: '目标半径',
29
+ enemyAir: '敌制空',
30
+ baseCount: '基地队数',
31
+ displayWaves: '显示波数',
32
+ targetState: '目标状态',
33
+ waveTarget: '第 {{base}} 队第 {{wave}} 波',
34
+ wave: '第 {{index}} 波',
35
+ optimize: '计算优化',
36
+ availablePlanes: '可用飞机',
37
+ candidatePlanes: '候选飞机',
38
+ noResult: '暂无结果',
39
+ noPoiState: '尚未读取到 Poi 数据,请在游戏数据加载后重试。',
40
+ noCandidateRadius: '没有可达半径 {{radius}} 的候选配装。',
41
+ plan: '方案',
42
+ attack: '攻击',
43
+ damagePower: '伤害基准',
44
+ worstMargin: '最小余量',
45
+ base: '第 {{index}} 队',
46
+ airPower: '制空',
47
+ radius: '半径',
48
+ denial: '劣势',
49
+ parity: '均势',
50
+ superiority: '优势',
51
+ supremacy: '确保',
52
+ loss: '丧失',
53
+ role_fighter: '制空',
54
+ role_attacker: '攻击',
55
+ role_recon: '侦察/延程',
56
+ role_unknown: '其他',
57
+ theoreticalPlanes: '理论候选',
58
+ minimumProficiency: '最低熟练度',
59
+ missingEquipment: '缺少装备',
60
+ missing: '未持有',
61
+ role_seaplaneBomber: '水爆',
62
+ enemyFleet: '敌舰队',
63
+ necessaryLines: '必要线',
64
+ clearComposition: '编成清空',
65
+ equipment: '装备',
66
+ lock: '锁定',
67
+ proficiency: '熟练',
68
+ baseSummary: '本队制空/半径/伤害',
69
+ baseColumn: '基地',
70
+ enemyShipName: '敌舰名',
71
+ ownedOnly: '仅持有装备',
72
+ includeMissing: '包含未持有理论装备',
73
+ importToSimulator: '导入到模拟器',
74
+ sixWaveState: '6波状态',
75
+ manualMode: '手动',
76
+ none: '无',
77
+ };
78
+
79
+ class LbasOptimizerPanel extends React.Component {
80
+ constructor(props) {
81
+ super(props);
82
+ this.state = {
83
+ simulator: createEmptySimulatorState(1),
84
+ equipmentCount: 0,
85
+ theoreticalCount: 0,
86
+ messages: [],
87
+ results: [],
88
+ };
89
+ }
90
+
91
+ runOptimizer = () => {
92
+ const t = getT();
93
+ const poiState = readPoiState();
94
+ if (!poiState) {
95
+ this.setState({
96
+ messages: [t('noPoiState')],
97
+ results: [],
98
+ equipmentCount: 0,
99
+ theoreticalCount: 0,
100
+ });
101
+ return;
102
+ }
103
+
104
+ const simulator = normalizeSimulatorState(this.state.simulator);
105
+ const optimizerInput = simulatorToOptimizerInput(simulator);
106
+ const ownedEquipment = extractOwnedPlanes(poiState);
107
+ const equipment = extractOptimizationPlanes(poiState, {
108
+ includeMissing: simulator.candidateMode === 'theoretical',
109
+ maxCopiesPerMaster: Number(simulator.baseCount) * 4,
110
+ });
111
+ const result = optimizeLoadouts({
112
+ ...optimizerInput,
113
+ equipment,
114
+ maxResults: 10,
115
+ });
116
+
117
+ this.setState({
118
+ simulator,
119
+ equipmentCount: ownedEquipment.length,
120
+ theoreticalCount: equipment.length,
121
+ messages: localizeMessages(result.messages, t),
122
+ results: result.results,
123
+ });
124
+ };
125
+
126
+ updateSimulator = (updater) => {
127
+ this.setState((state) => ({
128
+ simulator: normalizeSimulatorState(updater(state.simulator)),
129
+ }));
130
+ };
131
+
132
+ updateBaseCount = (baseCount) => {
133
+ this.updateSimulator((simulator) => setBaseCount(simulator, baseCount));
134
+ };
135
+
136
+ updateTargetRadius = (targetRadius) => {
137
+ this.updateSimulator((simulator) => ({
138
+ ...simulator,
139
+ targetRadius,
140
+ }));
141
+ };
142
+
143
+ updateEnemyAir = (enemyAir) => {
144
+ this.updateSimulator((simulator) => ({
145
+ ...simulator,
146
+ enemy: {
147
+ ...simulator.enemy,
148
+ enemyAir,
149
+ ships: simulator.enemy.ships.map((ship, index) =>
150
+ index === 0 ? { ...ship, airPower: enemyAir } : ship,
151
+ ),
152
+ },
153
+ }));
154
+ };
155
+
156
+ updateCandidateMode = (candidateMode) => {
157
+ this.updateSimulator((simulator) => ({
158
+ ...simulator,
159
+ candidateMode,
160
+ }));
161
+ };
162
+
163
+ updateSlotPlane = (baseIndex, slotIndex, instanceId) => {
164
+ const plane = findSelectablePlane(this.currentOwnedEquipment(), this.state.simulator, instanceId);
165
+ this.updateSimulator((simulator) => setBaseSlot(simulator, baseIndex, slotIndex, { plane }));
166
+ };
167
+
168
+ updateSlotLock = (baseIndex, slotIndex, locked) => {
169
+ this.updateSimulator((simulator) => setSlotLock(simulator, baseIndex, slotIndex, locked));
170
+ };
171
+
172
+ updateWaveTarget = (waveIndex, targetState) => {
173
+ this.updateSimulator((simulator) => setWaveTarget(simulator, waveIndex, targetState));
174
+ };
175
+
176
+ clearComposition = () => {
177
+ this.updateSimulator((simulator) => ({
178
+ ...simulator,
179
+ bases: simulator.bases.map((base) => ({
180
+ ...base,
181
+ slots: base.slots.map(() => ({
182
+ plane: null,
183
+ locked: false,
184
+ proficiency: null,
185
+ improvement: null,
186
+ })),
187
+ })),
188
+ }));
189
+ };
190
+
191
+ importPlan = (plan) => {
192
+ this.setState((state) => ({
193
+ simulator: applyPlanToSimulator(state.simulator, plan),
194
+ }));
195
+ };
196
+
197
+ currentOwnedEquipment() {
198
+ const poiState = readPoiState();
199
+ return poiState ? extractOwnedPlanes(poiState) : [];
200
+ }
201
+
202
+ render() {
203
+ const t = getT();
204
+ const simulator = normalizeSimulatorState(this.state.simulator);
205
+ const summary = calculateSimulatorSummary(simulator);
206
+ const ownedEquipment = this.currentOwnedEquipment();
207
+
208
+ return h(
209
+ 'div',
210
+ { style: styles.page },
211
+ h('h1', { style: styles.pageTitle }, t('title')),
212
+ h(SimulatorPanel, {
213
+ simulator,
214
+ summary,
215
+ equipment: ownedEquipment,
216
+ onBaseCountChange: this.updateBaseCount,
217
+ onTargetRadiusChange: this.updateTargetRadius,
218
+ onEnemyAirChange: this.updateEnemyAir,
219
+ onSlotPlaneChange: this.updateSlotPlane,
220
+ onSlotLockChange: this.updateSlotLock,
221
+ onWaveTargetChange: this.updateWaveTarget,
222
+ onClear: this.clearComposition,
223
+ t,
224
+ styles,
225
+ }),
226
+ h(OptimizerPanel, {
227
+ candidateMode: simulator.candidateMode,
228
+ equipmentCount: this.state.equipmentCount || ownedEquipment.length,
229
+ theoreticalCount: this.state.theoreticalCount || ownedEquipment.length,
230
+ messages: this.state.messages,
231
+ results: this.state.results,
232
+ onCandidateModeChange: this.updateCandidateMode,
233
+ onOptimize: this.runOptimizer,
234
+ onImportPlan: this.importPlan,
235
+ t,
236
+ styles,
237
+ }),
238
+ );
239
+ }
240
+ }
241
+
242
+ function readPoiState() {
243
+ if (typeof window === 'undefined' || typeof window.getStore !== 'function') {
244
+ return null;
245
+ }
246
+ return window.getStore();
247
+ }
248
+
249
+ function findSelectablePlane(equipment, simulator, instanceId) {
250
+ if (!instanceId) {
251
+ return null;
252
+ }
253
+ const wanted = String(instanceId);
254
+ const selected = simulator.bases.flatMap((base) => base.slots.map((slot) => slot.plane).filter(Boolean));
255
+ return [...equipment, ...selected].find((plane) => String(plane.instanceId) === wanted) || null;
256
+ }
257
+
258
+ function parseTargetStates(value) {
259
+ const states = Array.isArray(value)
260
+ ? value
261
+ : String(value || '').split(',');
262
+ const filtered = states
263
+ .map((state) => String(state).trim())
264
+ .filter((state) => STATE_OPTIONS.includes(state));
265
+ return filtered.length ? filtered : ['parity'];
266
+ }
267
+
268
+ function normalizeTargetStates(value, baseCount) {
269
+ const parsed = parseTargetStates(value);
270
+ const count = clamp(Number(baseCount) || 1, 1, 3);
271
+ return Array.from({ length: count * 2 }, (_, index) => parsed[index] || parsed[0] || 'parity');
272
+ }
273
+
274
+ function localizeMessages(messages, t) {
275
+ return messages.map((message) => {
276
+ const radiusMatch = message.match(/^No candidate loadout can reach radius (\d+)\.$/);
277
+ if (radiusMatch) {
278
+ return format(t('noCandidateRadius'), { radius: radiusMatch[1] });
279
+ }
280
+ return message;
281
+ });
282
+ }
283
+
284
+ function getT() {
285
+ try {
286
+ const i18next = require('views/env-parts/i18next').default;
287
+ const fixedT = i18next.getFixedT(null, PLUGIN_ID);
288
+ return (key) => fixedT(key);
289
+ } catch (error) {
290
+ return (key) => FALLBACK_ZH_CN[key] || key;
291
+ }
292
+ }
293
+
294
+ function format(template, values) {
295
+ return String(template).replace(/\{\{(\w+)\}\}/g, (_, key) => values[key] ?? '');
296
+ }
297
+
298
+ function clamp(value, min, max) {
299
+ return Math.max(min, Math.min(max, value));
300
+ }
301
+
302
+ function formatProficiency(level) {
303
+ if (level == null) {
304
+ return '-';
305
+ }
306
+ return ['-', '|', '||', '|||', '/', '//', '///', '>>'][level] || String(level);
307
+ }
308
+
309
+ const border = '1px solid rgba(128, 128, 128, 0.35)';
310
+ const styles = {
311
+ page: {
312
+ boxSizing: 'border-box',
313
+ fontFamily: 'sans-serif',
314
+ padding: 12,
315
+ },
316
+ pageTitle: {
317
+ fontSize: 18,
318
+ margin: '0 0 8px',
319
+ },
320
+ title: {
321
+ fontSize: 18,
322
+ margin: '0 0 8px',
323
+ },
324
+ sectionTitle: {
325
+ fontSize: 15,
326
+ margin: '0 0 8px',
327
+ },
328
+ simulatorPanel: {
329
+ border: border,
330
+ marginBottom: 12,
331
+ padding: 10,
332
+ },
333
+ simulatorGrid: {
334
+ display: 'grid',
335
+ gap: 10,
336
+ gridTemplateColumns: 'repeat(auto-fit, minmax(300px, 1fr))',
337
+ },
338
+ toolbar: {
339
+ alignItems: 'center',
340
+ display: 'flex',
341
+ flexWrap: 'wrap',
342
+ gap: 8,
343
+ marginBottom: 8,
344
+ },
345
+ field: {
346
+ display: 'grid',
347
+ fontSize: 12,
348
+ gap: 4,
349
+ },
350
+ fieldInline: {
351
+ alignItems: 'center',
352
+ display: 'inline-flex',
353
+ fontSize: 12,
354
+ gap: 6,
355
+ },
356
+ input: {
357
+ boxSizing: 'border-box',
358
+ fontSize: 13,
359
+ height: 28,
360
+ padding: '2px 6px',
361
+ width: '100%',
362
+ },
363
+ numberInput: {
364
+ boxSizing: 'border-box',
365
+ fontSize: 13,
366
+ height: 28,
367
+ padding: '2px 6px',
368
+ width: 72,
369
+ },
370
+ select: {
371
+ boxSizing: 'border-box',
372
+ fontSize: 13,
373
+ height: 28,
374
+ padding: '2px 6px',
375
+ width: '100%',
376
+ },
377
+ smallSelect: {
378
+ boxSizing: 'border-box',
379
+ fontSize: 13,
380
+ height: 28,
381
+ padding: '2px 6px',
382
+ },
383
+ button: {
384
+ cursor: 'pointer',
385
+ fontSize: 13,
386
+ minHeight: 28,
387
+ padding: '0 10px',
388
+ },
389
+ primaryButton: {
390
+ cursor: 'pointer',
391
+ fontSize: 13,
392
+ minHeight: 30,
393
+ padding: '0 14px',
394
+ },
395
+ meta: {
396
+ color: '#777',
397
+ fontSize: 12,
398
+ },
399
+ tableWrap: {
400
+ overflowX: 'auto',
401
+ },
402
+ table: {
403
+ borderCollapse: 'collapse',
404
+ fontSize: 13,
405
+ tableLayout: 'fixed',
406
+ width: '100%',
407
+ },
408
+ th: {
409
+ border: border,
410
+ fontWeight: 600,
411
+ padding: '5px 6px',
412
+ textAlign: 'center',
413
+ },
414
+ td: {
415
+ border: border,
416
+ padding: 5,
417
+ },
418
+ centerTd: {
419
+ border: border,
420
+ padding: 5,
421
+ textAlign: 'center',
422
+ },
423
+ baseName: {
424
+ border: border,
425
+ fontWeight: 600,
426
+ padding: 6,
427
+ textAlign: 'center',
428
+ width: 72,
429
+ },
430
+ summaryTd: {
431
+ border: border,
432
+ fontSize: 12,
433
+ lineHeight: 1.5,
434
+ padding: 6,
435
+ width: 150,
436
+ },
437
+ enemyPanel: {
438
+ minWidth: 0,
439
+ },
440
+ enemyControls: {
441
+ alignItems: 'end',
442
+ display: 'grid',
443
+ gap: 8,
444
+ gridTemplateColumns: '1fr auto',
445
+ marginBottom: 8,
446
+ },
447
+ manualTag: {
448
+ border: border,
449
+ fontSize: 12,
450
+ minHeight: 28,
451
+ padding: '5px 8px',
452
+ },
453
+ lines: {
454
+ border: border,
455
+ fontSize: 13,
456
+ lineHeight: 1.6,
457
+ marginTop: 8,
458
+ padding: 8,
459
+ },
460
+ wavePanel: {
461
+ border: border,
462
+ borderTop: 0,
463
+ padding: 8,
464
+ },
465
+ waveList: {
466
+ display: 'grid',
467
+ gap: 8,
468
+ gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))',
469
+ },
470
+ waveField: {
471
+ alignItems: 'center',
472
+ display: 'grid',
473
+ gap: 4,
474
+ gridTemplateColumns: 'auto 1fr auto',
475
+ },
476
+ goodState: {
477
+ color: '#2e7d32',
478
+ },
479
+ badState: {
480
+ color: '#b23b22',
481
+ },
482
+ optimizerPanel: {
483
+ border: border,
484
+ padding: 10,
485
+ },
486
+ optimizerControls: {
487
+ alignItems: 'center',
488
+ display: 'flex',
489
+ flexWrap: 'wrap',
490
+ gap: 10,
491
+ marginBottom: 8,
492
+ },
493
+ radioLabel: {
494
+ alignItems: 'center',
495
+ display: 'inline-flex',
496
+ gap: 4,
497
+ },
498
+ messages: {
499
+ color: '#d9534f',
500
+ fontSize: 12,
501
+ margin: '8px 0',
502
+ paddingLeft: 18,
503
+ },
504
+ emptyCell: {
505
+ border: border,
506
+ color: '#777',
507
+ padding: 10,
508
+ textAlign: 'center',
509
+ },
510
+ results: {
511
+ display: 'grid',
512
+ gap: 10,
513
+ },
514
+ plan: {
515
+ border: border,
516
+ padding: 10,
517
+ },
518
+ planHeader: {
519
+ alignItems: 'center',
520
+ display: 'flex',
521
+ flexWrap: 'wrap',
522
+ gap: 10,
523
+ marginBottom: 8,
524
+ },
525
+ planSummary: {
526
+ color: '#8a6d3b',
527
+ fontSize: 12,
528
+ marginBottom: 8,
529
+ },
530
+ waves: {
531
+ display: 'flex',
532
+ flexWrap: 'wrap',
533
+ gap: 6,
534
+ marginBottom: 8,
535
+ },
536
+ wave: {
537
+ border: '1px solid rgba(128, 128, 128, 0.25)',
538
+ fontSize: 12,
539
+ padding: '2px 6px',
540
+ },
541
+ loadout: {
542
+ fontSize: 12,
543
+ lineHeight: 1.5,
544
+ margin: 0,
545
+ paddingLeft: 18,
546
+ },
547
+ missingItem: {
548
+ color: '#777',
549
+ opacity: 0.6,
550
+ },
551
+ };
552
+
553
+ module.exports = {
554
+ formatProficiency,
555
+ reactClass: LbasOptimizerPanel,
556
+ parseTargetStates,
557
+ normalizeTargetStates,
558
+ };
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "poi-plugin-lbas-bis",
3
+ "version": "0.1.0",
4
+ "description": "Poi 陆航配装优化插件。",
5
+ "main": "index.js",
6
+ "type": "commonjs",
7
+ "scripts": {
8
+ "test": "vitest run",
9
+ "test:watch": "vitest",
10
+ "typecheck": "tsc --noEmit"
11
+ },
12
+ "keywords": [
13
+ "poi-plugin",
14
+ "kancolle",
15
+ "lbas"
16
+ ],
17
+ "files": [
18
+ "index.js",
19
+ "i18n",
20
+ "src",
21
+ "README.md",
22
+ "LICENSE"
23
+ ],
24
+ "license": "MIT",
25
+ "poiPlugin": {
26
+ "title": "陆航优化",
27
+ "id": "lbas_bis",
28
+ "description": "手动输入目标后计算基地航空队配装方案。",
29
+ "icon": "fa/plane",
30
+ "i18nDir": "i18n",
31
+ "priority": 52
32
+ },
33
+ "dependencies": {
34
+ "react": "18.3.1"
35
+ },
36
+ "devDependencies": {
37
+ "typescript": "^5.9.2",
38
+ "vitest": "^3.2.4"
39
+ }
40
+ }