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/LICENSE +21 -0
- package/README.md +69 -0
- package/i18n/en-US.json +56 -0
- package/i18n/ja-JP.json +56 -0
- package/i18n/zh-CN.json +56 -0
- package/i18n/zh-TW.json +56 -0
- package/index.js +558 -0
- package/package.json +40 -0
- package/src/air-power.js +183 -0
- package/src/damage.js +54 -0
- package/src/import-plan.js +31 -0
- package/src/optimizer.js +417 -0
- package/src/poi-data.js +154 -0
- package/src/simulator-calc.js +90 -0
- package/src/simulator-state.js +216 -0
- package/src/ui/BaseTable.js +134 -0
- package/src/ui/EnemyPanel.js +56 -0
- package/src/ui/OptimizerPanel.js +149 -0
- package/src/ui/SimulatorPanel.js +95 -0
- package/src/ui/WaveStatusTable.js +41 -0
package/src/poi-data.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const RECON_MASTER_IDS = new Set([138, 178, 311, 312]);
|
|
4
|
+
const LAND_BASED_API_TYPE_ROOTS = new Set([17, 21, 22, 25, 26]);
|
|
5
|
+
const AIRCRAFT_EQUIP_TYPES = new Set([
|
|
6
|
+
6, 7, 8, 9, 10, 11,
|
|
7
|
+
25, 26,
|
|
8
|
+
41, 45, 47, 48, 49,
|
|
9
|
+
53, 54, 56, 57, 58, 59,
|
|
10
|
+
]);
|
|
11
|
+
const FIGHTER_EQUIP_TYPES = new Set([6, 45, 48]);
|
|
12
|
+
const ATTACKER_EQUIP_TYPES = new Set([7, 8, 11, 47, 53, 57, 58, 59]);
|
|
13
|
+
const RECON_EQUIP_TYPES = new Set([9, 10, 41, 49]);
|
|
14
|
+
const SEAPLANE_BOMBER_EQUIP_TYPES = new Set([11]);
|
|
15
|
+
|
|
16
|
+
function extractOwnedPlanes(poiState) {
|
|
17
|
+
const masterById = getMasterEquipment(poiState);
|
|
18
|
+
const ownedEquips = Object.values(poiState?.info?.equips || {});
|
|
19
|
+
|
|
20
|
+
return ownedEquips
|
|
21
|
+
.map((equip) => toPlaneInstance(equip, masterById[equip.api_slotitem_id]))
|
|
22
|
+
.filter(Boolean);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function extractOptimizationPlanes(poiState, options = {}) {
|
|
26
|
+
const includeMissing = options.includeMissing === true;
|
|
27
|
+
const maxCopiesPerMaster = Math.max(1, Number(options.maxCopiesPerMaster) || 4);
|
|
28
|
+
const missingProficiency = Math.max(0, Math.min(7, Number(options.missingProficiency ?? 7) || 0));
|
|
29
|
+
const masterById = getMasterEquipment(poiState);
|
|
30
|
+
const ownedPlanes = extractOwnedPlanes(poiState).map((plane) => ({
|
|
31
|
+
...plane,
|
|
32
|
+
available: true,
|
|
33
|
+
missing: false,
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
if (!includeMissing) {
|
|
37
|
+
return ownedPlanes;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ownedCounts = new Map();
|
|
41
|
+
|
|
42
|
+
for (const plane of ownedPlanes) {
|
|
43
|
+
ownedCounts.set(plane.masterId, (ownedCounts.get(plane.masterId) || 0) + 1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const theoreticalPlanes = Object.values(masterById)
|
|
47
|
+
.filter(isLbasCandidateMaster)
|
|
48
|
+
.flatMap((master) => {
|
|
49
|
+
const masterId = Number(master.api_id) || 0;
|
|
50
|
+
const ownedCount = ownedCounts.get(masterId) || 0;
|
|
51
|
+
const missingCount = Math.max(0, maxCopiesPerMaster - ownedCount);
|
|
52
|
+
return Array.from({ length: missingCount }, (_, index) =>
|
|
53
|
+
toPlaneInstance(
|
|
54
|
+
{
|
|
55
|
+
api_id: `missing-${masterId}-${index + 1}`,
|
|
56
|
+
api_slotitem_id: masterId,
|
|
57
|
+
api_level: 0,
|
|
58
|
+
api_alv: missingProficiency,
|
|
59
|
+
},
|
|
60
|
+
master,
|
|
61
|
+
{
|
|
62
|
+
available: false,
|
|
63
|
+
missing: true,
|
|
64
|
+
copyIndex: index + 1,
|
|
65
|
+
},
|
|
66
|
+
),
|
|
67
|
+
);
|
|
68
|
+
})
|
|
69
|
+
.filter(Boolean);
|
|
70
|
+
|
|
71
|
+
return [...ownedPlanes, ...theoreticalPlanes];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getMasterEquipment(poiState) {
|
|
75
|
+
const constants = poiState?.const || {};
|
|
76
|
+
if (constants.$equips) {
|
|
77
|
+
return constants.$equips;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const items = constants.api_mst_slotitem || {};
|
|
81
|
+
if (Array.isArray(items)) {
|
|
82
|
+
return Object.fromEntries(items.map((item) => [item.api_id, item]));
|
|
83
|
+
}
|
|
84
|
+
return items;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function toPlaneInstance(equip, master, overrides = {}) {
|
|
88
|
+
if (!equip || !master || !isLbasCandidateMaster(master)) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const torpedo = Number(master.api_raig) || 0;
|
|
93
|
+
const bombing = Number(master.api_baku) || 0;
|
|
94
|
+
const antiAir = Number(master.api_tyku) || 0;
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
instanceId: equip.api_id,
|
|
98
|
+
masterId: master.api_id || equip.api_slotitem_id,
|
|
99
|
+
name: master.api_name || `Item ${equip.api_slotitem_id}`,
|
|
100
|
+
antiAir,
|
|
101
|
+
intercept: Number(master.api_houk) || 0,
|
|
102
|
+
antiBomber: Number(master.api_bakk) || 0,
|
|
103
|
+
radius: Number(master.api_distance) || 0,
|
|
104
|
+
improvement: Number(equip.api_level) || 0,
|
|
105
|
+
proficiency: Number(equip.api_alv) || 0,
|
|
106
|
+
role: classifyRole(master, { antiAir, torpedo, bombing }),
|
|
107
|
+
isLandBased: isLandBasedMaster(master),
|
|
108
|
+
torpedo,
|
|
109
|
+
bombing,
|
|
110
|
+
available: true,
|
|
111
|
+
missing: false,
|
|
112
|
+
...overrides,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isLbasCandidateMaster(master) {
|
|
117
|
+
const masterId = Number(master.api_id) || 0;
|
|
118
|
+
const equipType = getEquipType(master);
|
|
119
|
+
return (
|
|
120
|
+
(Number(master.api_distance) || 0) > 0 &&
|
|
121
|
+
(RECON_MASTER_IDS.has(masterId) || AIRCRAFT_EQUIP_TYPES.has(equipType))
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function classifyRole(master, stats) {
|
|
126
|
+
const masterId = Number(master.api_id) || 0;
|
|
127
|
+
const equipType = getEquipType(master);
|
|
128
|
+
if (RECON_MASTER_IDS.has(masterId) || RECON_EQUIP_TYPES.has(equipType)) {
|
|
129
|
+
return 'recon';
|
|
130
|
+
}
|
|
131
|
+
if (SEAPLANE_BOMBER_EQUIP_TYPES.has(equipType)) {
|
|
132
|
+
return 'seaplaneBomber';
|
|
133
|
+
}
|
|
134
|
+
if (ATTACKER_EQUIP_TYPES.has(equipType) || stats.torpedo > 0 || stats.bombing > 0) {
|
|
135
|
+
return 'attacker';
|
|
136
|
+
}
|
|
137
|
+
if (FIGHTER_EQUIP_TYPES.has(equipType) || stats.antiAir > 0) {
|
|
138
|
+
return 'fighter';
|
|
139
|
+
}
|
|
140
|
+
return 'unknown';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function getEquipType(master) {
|
|
144
|
+
return Number(master.api_type?.[2]) || 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function isLandBasedMaster(master) {
|
|
148
|
+
return LAND_BASED_API_TYPE_ROOTS.has(Number(master.api_type?.[0]) || 0);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = {
|
|
152
|
+
extractOptimizationPlanes,
|
|
153
|
+
extractOwnedPlanes,
|
|
154
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
airStateFor,
|
|
5
|
+
calculateBaseAirPower,
|
|
6
|
+
calculateEffectiveRadius,
|
|
7
|
+
requiredAirForState,
|
|
8
|
+
} = require('./air-power');
|
|
9
|
+
const { calculateBaseDamagePower } = require('./damage');
|
|
10
|
+
const { normalizeSimulatorState } = require('./simulator-state');
|
|
11
|
+
|
|
12
|
+
function calculateEnemyAirLines(enemyAir) {
|
|
13
|
+
return {
|
|
14
|
+
supremacy: requiredAirForState(enemyAir, 'supremacy'),
|
|
15
|
+
superiority: requiredAirForState(enemyAir, 'superiority'),
|
|
16
|
+
parity: requiredAirForState(enemyAir, 'parity'),
|
|
17
|
+
denial: requiredAirForState(enemyAir, 'denial'),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function calculateSimulatorSummary(state) {
|
|
22
|
+
const normalized = normalizeSimulatorState(state);
|
|
23
|
+
const enemyAir = normalized.enemy.enemyAir;
|
|
24
|
+
const bases = normalized.bases.map((base, baseIndex) => {
|
|
25
|
+
const loadout = base.slots.map((slot) => slot.plane).filter(Boolean);
|
|
26
|
+
const airPower = calculateBaseAirPower(loadout);
|
|
27
|
+
const radius = calculateEffectiveRadius(loadout);
|
|
28
|
+
const damagePower = calculateBaseDamagePower(loadout);
|
|
29
|
+
const stateForBase = airStateFor(airPower, enemyAir);
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
baseIndex,
|
|
33
|
+
name: base.name,
|
|
34
|
+
airPower,
|
|
35
|
+
radius,
|
|
36
|
+
damagePower,
|
|
37
|
+
state: stateForBase,
|
|
38
|
+
loadout,
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
const waves = normalized.waves.map((wave, waveIndex) => {
|
|
42
|
+
const base = bases[wave.baseIndex] || bases[0] || emptyBaseSummary();
|
|
43
|
+
const stateForWave = airStateFor(base.airPower, enemyAir);
|
|
44
|
+
return {
|
|
45
|
+
waveIndex,
|
|
46
|
+
baseIndex: wave.baseIndex,
|
|
47
|
+
waveInBase: wave.waveInBase,
|
|
48
|
+
targetState: wave.targetState,
|
|
49
|
+
airPower: base.airPower,
|
|
50
|
+
state: stateForWave,
|
|
51
|
+
fulfilled: stateForWave.rank >= airStateRank(wave.targetState),
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
bases,
|
|
57
|
+
waves,
|
|
58
|
+
enemyAirLines: calculateEnemyAirLines(enemyAir),
|
|
59
|
+
totalAirPower: bases.reduce((total, base) => total + base.airPower, 0),
|
|
60
|
+
statusKey: weakestStateKey(waves),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function emptyBaseSummary() {
|
|
65
|
+
return {
|
|
66
|
+
airPower: 0,
|
|
67
|
+
radius: 0,
|
|
68
|
+
damagePower: 0,
|
|
69
|
+
state: airStateFor(0, 0),
|
|
70
|
+
loadout: [],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function airStateRank(stateKey) {
|
|
75
|
+
return airStateFor(requiredAirForState(100, stateKey), 100).rank;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function weakestStateKey(waves) {
|
|
79
|
+
if (!waves.length) {
|
|
80
|
+
return 'loss';
|
|
81
|
+
}
|
|
82
|
+
return waves.reduce((weakest, wave) =>
|
|
83
|
+
wave.state.rank < weakest.rank ? wave.state : weakest,
|
|
84
|
+
waves[0].state).key;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = {
|
|
88
|
+
calculateEnemyAirLines,
|
|
89
|
+
calculateSimulatorSummary,
|
|
90
|
+
};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const SLOTS_PER_BASE = 4;
|
|
4
|
+
const WAVES_PER_BASE = 2;
|
|
5
|
+
const MIN_BASES = 1;
|
|
6
|
+
const MAX_BASES = 3;
|
|
7
|
+
const DEFAULT_TARGET_RADIUS = 7;
|
|
8
|
+
const DEFAULT_ENEMY_AIR = 72;
|
|
9
|
+
const DEFAULT_TARGET_STATE = 'parity';
|
|
10
|
+
const STATE_OPTIONS = new Set(['denial', 'parity', 'superiority', 'supremacy']);
|
|
11
|
+
const BASE_NAMES = ['第一基地', '第二基地', '第三基地'];
|
|
12
|
+
|
|
13
|
+
function createEmptySimulatorState(baseCount = 1) {
|
|
14
|
+
return normalizeSimulatorState({
|
|
15
|
+
targetRadius: DEFAULT_TARGET_RADIUS,
|
|
16
|
+
baseCount,
|
|
17
|
+
candidateMode: 'owned',
|
|
18
|
+
enemy: createDefaultEnemy(),
|
|
19
|
+
bases: [],
|
|
20
|
+
waves: [],
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeSimulatorState(state = {}) {
|
|
25
|
+
const baseCount = clampBaseCount(state.baseCount);
|
|
26
|
+
return {
|
|
27
|
+
targetRadius: positiveNumber(state.targetRadius, DEFAULT_TARGET_RADIUS),
|
|
28
|
+
baseCount,
|
|
29
|
+
candidateMode: state.candidateMode === 'theoretical' ? 'theoretical' : 'owned',
|
|
30
|
+
enemy: normalizeEnemy(state.enemy),
|
|
31
|
+
bases: normalizeBases(state.bases, baseCount),
|
|
32
|
+
waves: normalizeWaves(state.waves, baseCount),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function setBaseCount(state, baseCount) {
|
|
37
|
+
return normalizeSimulatorState({
|
|
38
|
+
...state,
|
|
39
|
+
baseCount,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function setBaseSlot(state, baseIndex, slotIndex, slotPatch) {
|
|
44
|
+
const normalized = normalizeSimulatorState(state);
|
|
45
|
+
if (!isValidBaseSlot(baseIndex, slotIndex, normalized.baseCount)) {
|
|
46
|
+
return normalized;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const bases = normalized.bases.map((base, currentBaseIndex) => {
|
|
50
|
+
if (currentBaseIndex !== baseIndex) {
|
|
51
|
+
return base;
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
...base,
|
|
55
|
+
slots: base.slots.map((slot, currentSlotIndex) =>
|
|
56
|
+
currentSlotIndex === slotIndex
|
|
57
|
+
? { ...slot, ...slotPatch }
|
|
58
|
+
: slot,
|
|
59
|
+
),
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return normalizeSimulatorState({
|
|
64
|
+
...normalized,
|
|
65
|
+
bases,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function setSlotLock(state, baseIndex, slotIndex, locked) {
|
|
70
|
+
return setBaseSlot(state, baseIndex, slotIndex, { locked: Boolean(locked) });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function setWaveTarget(state, waveIndex, targetState) {
|
|
74
|
+
const normalized = normalizeSimulatorState(state);
|
|
75
|
+
if (waveIndex < 0 || waveIndex >= normalized.waves.length) {
|
|
76
|
+
return normalized;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const waves = normalized.waves.map((wave, currentIndex) =>
|
|
80
|
+
currentIndex === waveIndex
|
|
81
|
+
? { ...wave, targetState: normalizeTargetState(targetState) }
|
|
82
|
+
: wave,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
return normalizeSimulatorState({
|
|
86
|
+
...normalized,
|
|
87
|
+
waves,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function simulatorToOptimizerInput(state) {
|
|
92
|
+
const normalized = normalizeSimulatorState(state);
|
|
93
|
+
return {
|
|
94
|
+
baseCount: normalized.baseCount,
|
|
95
|
+
targetRadius: normalized.targetRadius,
|
|
96
|
+
enemyAir: normalized.enemy.enemyAir,
|
|
97
|
+
targetStates: normalized.waves.map((wave) => wave.targetState),
|
|
98
|
+
lockedBases: normalized.bases.map((base) => ({
|
|
99
|
+
slots: base.slots.map((slot) => ({
|
|
100
|
+
plane: slot.locked ? slot.plane : null,
|
|
101
|
+
locked: Boolean(slot.locked),
|
|
102
|
+
})),
|
|
103
|
+
})),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function createDefaultEnemy() {
|
|
108
|
+
return {
|
|
109
|
+
mode: 'manual',
|
|
110
|
+
enemyAir: DEFAULT_ENEMY_AIR,
|
|
111
|
+
areaId: null,
|
|
112
|
+
nodeId: null,
|
|
113
|
+
ships: Array.from({ length: 6 }, (_, index) => ({
|
|
114
|
+
id: null,
|
|
115
|
+
name: '',
|
|
116
|
+
airPower: index === 0 ? DEFAULT_ENEMY_AIR : 0,
|
|
117
|
+
})),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function normalizeEnemy(enemy = {}) {
|
|
122
|
+
const defaults = createDefaultEnemy();
|
|
123
|
+
const ships = Array.from({ length: 6 }, (_, index) => {
|
|
124
|
+
const ship = enemy.ships?.[index] || defaults.ships[index];
|
|
125
|
+
return {
|
|
126
|
+
id: ship.id ?? null,
|
|
127
|
+
name: ship.name || '',
|
|
128
|
+
airPower: nonNegativeNumber(ship.airPower, defaults.ships[index].airPower),
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
const summedAir = ships.reduce((total, ship) => total + ship.airPower, 0);
|
|
132
|
+
const enemyAir = enemy.enemyAir == null
|
|
133
|
+
? summedAir
|
|
134
|
+
: nonNegativeNumber(enemy.enemyAir, DEFAULT_ENEMY_AIR);
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
mode: enemy.mode || 'manual',
|
|
138
|
+
enemyAir,
|
|
139
|
+
areaId: enemy.areaId ?? null,
|
|
140
|
+
nodeId: enemy.nodeId ?? null,
|
|
141
|
+
ships,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function normalizeBases(bases = [], baseCount) {
|
|
146
|
+
return Array.from({ length: baseCount }, (_, baseIndex) => {
|
|
147
|
+
const base = bases[baseIndex] || {};
|
|
148
|
+
return {
|
|
149
|
+
name: base.name || BASE_NAMES[baseIndex] || `Base ${baseIndex + 1}`,
|
|
150
|
+
slots: normalizeSlots(base.slots),
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function normalizeSlots(slots = []) {
|
|
156
|
+
return Array.from({ length: SLOTS_PER_BASE }, (_, slotIndex) => {
|
|
157
|
+
const slot = slots[slotIndex] || {};
|
|
158
|
+
return {
|
|
159
|
+
plane: slot.plane || null,
|
|
160
|
+
locked: Boolean(slot.locked),
|
|
161
|
+
proficiency: slot.proficiency ?? null,
|
|
162
|
+
improvement: slot.improvement ?? null,
|
|
163
|
+
};
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function normalizeWaves(waves = [], baseCount) {
|
|
168
|
+
return Array.from({ length: baseCount * WAVES_PER_BASE }, (_, waveIndex) => {
|
|
169
|
+
const wave = waves[waveIndex] || {};
|
|
170
|
+
return {
|
|
171
|
+
baseIndex: Math.floor(waveIndex / WAVES_PER_BASE),
|
|
172
|
+
waveInBase: waveIndex % WAVES_PER_BASE,
|
|
173
|
+
targetState: normalizeTargetState(wave.targetState),
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function normalizeTargetState(targetState) {
|
|
179
|
+
return STATE_OPTIONS.has(targetState) ? targetState : DEFAULT_TARGET_STATE;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function isValidBaseSlot(baseIndex, slotIndex, baseCount) {
|
|
183
|
+
return (
|
|
184
|
+
baseIndex >= 0 &&
|
|
185
|
+
baseIndex < baseCount &&
|
|
186
|
+
slotIndex >= 0 &&
|
|
187
|
+
slotIndex < SLOTS_PER_BASE
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function clampBaseCount(value) {
|
|
192
|
+
const count = Number(value) || MIN_BASES;
|
|
193
|
+
return Math.max(MIN_BASES, Math.min(MAX_BASES, Math.floor(count)));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function positiveNumber(value, fallback) {
|
|
197
|
+
const number = Number(value);
|
|
198
|
+
return Number.isFinite(number) && number > 0 ? number : fallback;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function nonNegativeNumber(value, fallback) {
|
|
202
|
+
const number = Number(value);
|
|
203
|
+
return Number.isFinite(number) && number >= 0 ? number : fallback;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
module.exports = {
|
|
207
|
+
SLOTS_PER_BASE,
|
|
208
|
+
WAVES_PER_BASE,
|
|
209
|
+
createEmptySimulatorState,
|
|
210
|
+
normalizeSimulatorState,
|
|
211
|
+
setBaseCount,
|
|
212
|
+
setBaseSlot,
|
|
213
|
+
setSlotLock,
|
|
214
|
+
setWaveTarget,
|
|
215
|
+
simulatorToOptimizerInput,
|
|
216
|
+
};
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const React = require('react');
|
|
4
|
+
|
|
5
|
+
const h = React.createElement;
|
|
6
|
+
const PROFICIENCY_LABELS = ['-', '|', '||', '|||', '/', '//', '///', '>>'];
|
|
7
|
+
|
|
8
|
+
function BaseTable(props) {
|
|
9
|
+
const { bases, equipment, summaries, onSlotPlaneChange, onSlotLockChange, t, styles } = props;
|
|
10
|
+
return h(
|
|
11
|
+
'div',
|
|
12
|
+
{ style: styles.tableWrap },
|
|
13
|
+
h(
|
|
14
|
+
'table',
|
|
15
|
+
{ style: styles.table },
|
|
16
|
+
h(
|
|
17
|
+
'thead',
|
|
18
|
+
null,
|
|
19
|
+
h(
|
|
20
|
+
'tr',
|
|
21
|
+
null,
|
|
22
|
+
h('th', { style: styles.th }, t('baseColumn')),
|
|
23
|
+
h('th', { style: styles.th }, t('equipment')),
|
|
24
|
+
h('th', { style: styles.th }, t('lock')),
|
|
25
|
+
h('th', { style: styles.th }, t('proficiency')),
|
|
26
|
+
h('th', { style: styles.th }, t('baseSummary')),
|
|
27
|
+
),
|
|
28
|
+
),
|
|
29
|
+
h(
|
|
30
|
+
'tbody',
|
|
31
|
+
null,
|
|
32
|
+
bases.flatMap((base, baseIndex) =>
|
|
33
|
+
base.slots.map((slot, slotIndex) =>
|
|
34
|
+
h(
|
|
35
|
+
'tr',
|
|
36
|
+
{ key: `base-${baseIndex}-slot-${slotIndex}` },
|
|
37
|
+
slotIndex === 0
|
|
38
|
+
? h('td', { rowSpan: base.slots.length, style: styles.baseName }, base.name)
|
|
39
|
+
: null,
|
|
40
|
+
h(
|
|
41
|
+
'td',
|
|
42
|
+
{ style: styles.td },
|
|
43
|
+
renderEquipmentSelect({
|
|
44
|
+
equipment,
|
|
45
|
+
slot,
|
|
46
|
+
baseIndex,
|
|
47
|
+
slotIndex,
|
|
48
|
+
onSlotPlaneChange,
|
|
49
|
+
t,
|
|
50
|
+
styles,
|
|
51
|
+
}),
|
|
52
|
+
),
|
|
53
|
+
h(
|
|
54
|
+
'td',
|
|
55
|
+
{ style: styles.centerTd },
|
|
56
|
+
h('input', {
|
|
57
|
+
type: 'checkbox',
|
|
58
|
+
checked: Boolean(slot.locked),
|
|
59
|
+
onChange: (event) => onSlotLockChange(baseIndex, slotIndex, event.target.checked),
|
|
60
|
+
title: t('lock'),
|
|
61
|
+
}),
|
|
62
|
+
),
|
|
63
|
+
h(
|
|
64
|
+
'td',
|
|
65
|
+
{ style: styles.centerTd },
|
|
66
|
+
h(
|
|
67
|
+
'select',
|
|
68
|
+
{ value: slot.plane?.proficiency ?? '', disabled: true, style: styles.smallSelect },
|
|
69
|
+
h('option', { value: '' }, '-'),
|
|
70
|
+
PROFICIENCY_LABELS.map((label, index) =>
|
|
71
|
+
h('option', { key: index, value: index }, label),
|
|
72
|
+
),
|
|
73
|
+
),
|
|
74
|
+
),
|
|
75
|
+
slotIndex === 0
|
|
76
|
+
? h(
|
|
77
|
+
'td',
|
|
78
|
+
{ rowSpan: base.slots.length, style: styles.summaryTd },
|
|
79
|
+
formatSummary(summaries[baseIndex], t),
|
|
80
|
+
)
|
|
81
|
+
: null,
|
|
82
|
+
),
|
|
83
|
+
),
|
|
84
|
+
),
|
|
85
|
+
),
|
|
86
|
+
),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function renderEquipmentSelect(props) {
|
|
91
|
+
const { equipment, slot, baseIndex, slotIndex, onSlotPlaneChange, t, styles } = props;
|
|
92
|
+
const options = optionPlanes(equipment, slot.plane);
|
|
93
|
+
return h(
|
|
94
|
+
'select',
|
|
95
|
+
{
|
|
96
|
+
value: slot.plane ? String(slot.plane.instanceId) : '',
|
|
97
|
+
onChange: (event) => onSlotPlaneChange(baseIndex, slotIndex, event.target.value),
|
|
98
|
+
style: styles.select,
|
|
99
|
+
},
|
|
100
|
+
h('option', { value: '' }, t('none')),
|
|
101
|
+
options.map((plane) =>
|
|
102
|
+
h(
|
|
103
|
+
'option',
|
|
104
|
+
{ key: String(plane.instanceId), value: String(plane.instanceId) },
|
|
105
|
+
`${plane.name} #${plane.instanceId}${plane.missing ? ' (未持有)' : ''}`,
|
|
106
|
+
),
|
|
107
|
+
),
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function optionPlanes(equipment, currentPlane) {
|
|
112
|
+
const unique = new Map();
|
|
113
|
+
for (const plane of equipment || []) {
|
|
114
|
+
unique.set(String(plane.instanceId), plane);
|
|
115
|
+
}
|
|
116
|
+
if (currentPlane && !unique.has(String(currentPlane.instanceId))) {
|
|
117
|
+
unique.set(String(currentPlane.instanceId), currentPlane);
|
|
118
|
+
}
|
|
119
|
+
return [...unique.values()];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function formatSummary(summary, t) {
|
|
123
|
+
if (!summary) {
|
|
124
|
+
return '-';
|
|
125
|
+
}
|
|
126
|
+
return [
|
|
127
|
+
`${t('airPower')} ${summary.airPower}`,
|
|
128
|
+
`${t('radius')} ${summary.radius}`,
|
|
129
|
+
`${t('damagePower')} ${summary.damagePower}`,
|
|
130
|
+
t(summary.state.key),
|
|
131
|
+
].join(' / ');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = BaseTable;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const React = require('react');
|
|
4
|
+
|
|
5
|
+
const h = React.createElement;
|
|
6
|
+
|
|
7
|
+
function EnemyPanel(props) {
|
|
8
|
+
const { enemy, lines, onEnemyAirChange, t, styles } = props;
|
|
9
|
+
return h(
|
|
10
|
+
'section',
|
|
11
|
+
{ style: styles.enemyPanel },
|
|
12
|
+
h('h3', { style: styles.sectionTitle }, t('enemyFleet')),
|
|
13
|
+
h(
|
|
14
|
+
'div',
|
|
15
|
+
{ style: styles.enemyControls },
|
|
16
|
+
h(
|
|
17
|
+
'label',
|
|
18
|
+
{ style: styles.field },
|
|
19
|
+
h('span', null, t('enemyAir')),
|
|
20
|
+
h('input', {
|
|
21
|
+
type: 'number',
|
|
22
|
+
min: 0,
|
|
23
|
+
value: enemy.enemyAir,
|
|
24
|
+
onChange: (event) => onEnemyAirChange(Number(event.target.value)),
|
|
25
|
+
style: styles.input,
|
|
26
|
+
}),
|
|
27
|
+
),
|
|
28
|
+
h('div', { style: styles.manualTag }, t('manualMode')),
|
|
29
|
+
),
|
|
30
|
+
h(
|
|
31
|
+
'table',
|
|
32
|
+
{ style: styles.table },
|
|
33
|
+
h('thead', null, h('tr', null, h('th', { style: styles.th }, t('enemyShipName')), h('th', { style: styles.th }, t('airPower')))),
|
|
34
|
+
h(
|
|
35
|
+
'tbody',
|
|
36
|
+
null,
|
|
37
|
+
enemy.ships.map((ship, index) =>
|
|
38
|
+
h(
|
|
39
|
+
'tr',
|
|
40
|
+
{ key: `enemy-${index}` },
|
|
41
|
+
h('td', { style: styles.td }, ship.name || t('none')),
|
|
42
|
+
h('td', { style: styles.centerTd }, index === 0 ? enemy.enemyAir : ship.airPower),
|
|
43
|
+
),
|
|
44
|
+
),
|
|
45
|
+
),
|
|
46
|
+
),
|
|
47
|
+
h(
|
|
48
|
+
'div',
|
|
49
|
+
{ style: styles.lines },
|
|
50
|
+
h('strong', null, `${t('necessaryLines')}: `),
|
|
51
|
+
`${t('supremacy')} ${lines.supremacy} / ${t('superiority')} ${lines.superiority} / ${t('parity')} ${lines.parity} / ${t('denial')} ${lines.denial}`,
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = EnemyPanel;
|