poi-plugin-equips-farm 1.0.2

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.
@@ -0,0 +1,273 @@
1
+ import React, { Component, useState, useEffect } from 'react'
2
+ import { Card, Button, InputGroup, Tag, Collapse, Divider, NumericInput, Classes, ControlGroup } from '@blueprintjs/core'
3
+ import { Avatar } from 'views/components/etc/avatar'
4
+ import { SlotitemIcon } from 'views/components/etc/icon'
5
+ import { checkQuota } from '../../lib/data-processor'
6
+ import { matchesSearch } from '../../lib/search-utils'
7
+
8
+ // Robust Control for Redux-bound Inputs
9
+ const TargetControl = ({ id, count, onUpdate }) => {
10
+ const [val, setVal] = useState(String(count || 0))
11
+
12
+ useEffect(() => {
13
+ setVal(String(count || 0))
14
+ }, [count])
15
+
16
+ const handleConfirm = (newValStr) => {
17
+ let finalVal = parseInt(newValStr)
18
+ if (isNaN(finalVal) || finalVal < 0) finalVal = 0
19
+ setVal(String(finalVal))
20
+ onUpdate(id, finalVal)
21
+ }
22
+
23
+ return (
24
+ <ControlGroup style={{ marginLeft: 10 }}>
25
+ <Button
26
+ icon="minus"
27
+ onClick={() => handleConfirm(String((parseInt(val) || 0) - 1))}
28
+ disabled={(parseInt(val) || 0) <= 0}
29
+ />
30
+ <InputGroup
31
+ value={val}
32
+ onChange={(e) => setVal(e.target.value)}
33
+ onBlur={(e) => handleConfirm(e.target.value)}
34
+ onKeyDown={(e) => {
35
+ if (e.key === 'Enter') handleConfirm(e.currentTarget.value)
36
+ }}
37
+ style={{ width: 50, textAlign: 'center', zIndex: 0 }}
38
+ />
39
+ <Button
40
+ icon="plus"
41
+ onClick={() => handleConfirm(String((parseInt(val) || 0) + 1))}
42
+ />
43
+ </ControlGroup>
44
+ )
45
+ }
46
+
47
+ export default class EquipmentList extends Component {
48
+ constructor(props) {
49
+ super(props)
50
+ this.state = {
51
+ filterType: 'All', // All, Marked, Unmarked
52
+ search: '',
53
+ expandedId: null,
54
+ selectedTypeIds: new Set()
55
+ }
56
+ }
57
+
58
+ handleToggleExpand = (id) => {
59
+ this.setState(prev => ({ expandedId: prev.expandedId === id ? null : id }))
60
+ }
61
+
62
+ handleTypeToggle = (typeId) => {
63
+ this.setState(prev => {
64
+ const newSet = new Set(prev.selectedTypeIds)
65
+ if (newSet.has(typeId)) {
66
+ newSet.delete(typeId)
67
+ } else {
68
+ newSet.add(typeId)
69
+ }
70
+ return { selectedTypeIds: newSet }
71
+ })
72
+ }
73
+
74
+ render() {
75
+ // Props contain full equip list + master data
76
+ const { equipments, targets, onAdd, onRemove, userEquips, userShips, farmingMap, $equipTypes, $ships } = this.props
77
+ const { filterType, search, expandedId } = this.state
78
+
79
+ // 1. Initial Filter (Search & Status)
80
+ let filtered = equipments.filter(eq => {
81
+ const isMarked = !!targets[eq.id]
82
+ if (filterType === 'Marked' && !isMarked) return false
83
+ if (filterType === 'Unmarked' && isMarked) return false
84
+ // Enhanced multi-language search: supports Chinese, Japanese, Pinyin, Romaji
85
+ if (search && !matchesSearch(search, eq)) return false
86
+ return true
87
+ })
88
+
89
+ // 2. Prepare Types Data for Grid Filter (Dynamic based on available equipment)
90
+ const availableTypesMap = {}
91
+ equipments.forEach(eq => {
92
+ const tId = eq.typeId || 999
93
+ if (!availableTypesMap[tId]) {
94
+ const typeInfo = $equipTypes[tId]
95
+ availableTypesMap[tId] = {
96
+ id: tId,
97
+ name: typeInfo ? typeInfo.api_name : 'Others',
98
+ iconId: eq.iconId
99
+ }
100
+ }
101
+ })
102
+ const availableTypes = Object.values(availableTypesMap).sort((a,b) => a.id - b.id)
103
+
104
+ // 3. Apply Type Filter
105
+ const { selectedTypeIds } = this.state
106
+ if (selectedTypeIds.size > 0) {
107
+ filtered = filtered.filter(eq => selectedTypeIds.has(eq.typeId || 999))
108
+ }
109
+
110
+ // 4. Group by Category
111
+ const groups = {}
112
+ filtered.forEach(eq => {
113
+ const typeId = eq.typeId || 999
114
+ if (!groups[typeId]) groups[typeId] = []
115
+ groups[typeId].push(eq)
116
+ })
117
+
118
+ const sortedTypeIds = Object.keys(groups).sort((a,b) => parseInt(a) - parseInt(b))
119
+
120
+ return (
121
+ <div className="equipment-list-container" style={{ display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0 }}>
122
+ {/* Top Controls */}
123
+ <div className="filters" style={{ marginBottom: 10, flexShrink: 0, padding: 2 }}>
124
+ <div style={{ display: 'flex', marginBottom: 10 }}>
125
+ <InputGroup
126
+ leftIcon="search"
127
+ placeholder="Search equipment..."
128
+ value={search}
129
+ onChange={(e) => this.setState({ search: e.target.value })}
130
+ style={{ flex: 1, marginRight: 10 }}
131
+ />
132
+ <div className="bp3-button-group">
133
+ <Button active={filterType === 'All'} onClick={() => this.setState({ filterType: 'All' })} className="bp3-small">All</Button>
134
+ <Button active={filterType === 'Marked'} intent={filterType === 'Marked' ? "primary" : "none"} onClick={() => this.setState({ filterType: 'Marked' })} className="bp3-small">Marked</Button>
135
+ <Button active={filterType === 'Unmarked'} onClick={() => this.setState({ filterType: 'Unmarked' })} className="bp3-small">Unmarked</Button>
136
+ </div>
137
+ </div>
138
+
139
+ {/* Type Filter Grid */}
140
+ <div className="type-filter-grid" style={{
141
+ display: 'grid',
142
+ gridTemplateColumns: 'repeat(auto-fill, minmax(60px, 1fr))', // Auto grid
143
+ gap: '8px',
144
+ padding: '10px 0', // Remove horizontal padding if bg is gone
145
+ // background: 'rgba(33, 33, 33, 0.3)', // Removed
146
+ maxHeight: '150px',
147
+ overflowY: 'auto',
148
+ marginBottom: '10px'
149
+ }}>
150
+ {availableTypes.map(t => {
151
+ const isSelected = selectedTypeIds.has(t.id)
152
+ return (
153
+ <div
154
+ key={t.id}
155
+ onClick={() => this.handleTypeToggle(t.id)}
156
+ title={t.name}
157
+ style={{
158
+ display: 'flex',
159
+ alignItems: 'center',
160
+ cursor: 'pointer',
161
+ opacity: isSelected ? 1 : 0.6,
162
+ justifyContent: 'flex-start' // Align left in grid cell
163
+ }}
164
+ >
165
+ {/* Custom Checkbox Look */}
166
+ <div style={{
167
+ width: '14px',
168
+ height: '14px',
169
+ border: '1px solid #888',
170
+ background: isSelected ? '#2196F3' : 'transparent',
171
+ marginRight: '6px',
172
+ display: 'flex',
173
+ alignItems: 'center',
174
+ justifyContent: 'center',
175
+ borderRadius: '2px',
176
+ flexShrink: 0
177
+ }}>
178
+ {isSelected && <div style={{ width: '8px', height: '8px', background: '#fff' }} />}
179
+ </div>
180
+ <SlotitemIcon slotitemId={t.iconId} style={{ width: 24, height: 24 }} />
181
+ </div>
182
+ )
183
+ })}
184
+ {availableTypes.length === 0 && <span className="bp3-text-muted">Loading types...</span>}
185
+ </div>
186
+ </div>
187
+
188
+ {/* List Content */}
189
+ <div className="list-content" style={{ flex: 1, overflowY: 'auto', paddingRight: 5 }}>
190
+ {sortedTypeIds.map(typeId => {
191
+ const groupItems = groups[typeId]
192
+ // const typeInfo = $equipTypes[typeId]
193
+ // const typeName = typeInfo ? typeInfo.api_name : 'Others'
194
+
195
+ return (
196
+ <div key={typeId} className="type-group" style={{ marginBottom: 0 }}>
197
+ {/* Header Removed per user request */}
198
+
199
+ {groupItems.map(eq => {
200
+ const targetCount = targets[eq.id] || 0
201
+ const isMarked = targetCount > 0
202
+ const isExpanded = expandedId === eq.id
203
+ const quota = checkQuota(targetCount, eq.id, userEquips, userShips, farmingMap)
204
+
205
+ return (
206
+ <Card key={eq.id} elevation={0} style={{ marginBottom: 8, border: '1px solid #ddd', padding: 8 }}>
207
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
208
+
209
+ <div
210
+ style={{ display: 'flex', alignItems: 'center', cursor: 'pointer', flex: 1 }}
211
+ onClick={() => this.handleToggleExpand(eq.id)}
212
+ >
213
+ <SlotitemIcon slotitemId={eq.iconId} style={{ marginRight: 10, width: 30, height: 30 }} />
214
+ <div style={{ lineHeight: '1.2' }}>
215
+ <div style={{ fontWeight: 'bold' }}>{eq.name}</div>
216
+ </div>
217
+ </div>
218
+
219
+ <div style={{ display: 'flex', alignItems: 'center' }}>
220
+ {isMarked && (
221
+ <div style={{ marginRight: 10, textAlign: 'right', fontSize: '0.8em', lineHeight: 1.1 }}>
222
+ <div className={quota.isSatisfied ? "bp3-text-success" : "bp3-text-warning"}>
223
+ {quota.current}/{targetCount}
224
+ </div>
225
+ </div>
226
+ )}
227
+ <TargetControl
228
+ id={parseInt(eq.id)}
229
+ count={targetCount}
230
+ onUpdate={(id, c) => {
231
+ if (c <= 0) onRemove(id)
232
+ else onAdd(id, c)
233
+ }}
234
+ />
235
+ </div>
236
+ </div>
237
+
238
+ <Collapse isOpen={isExpanded}>
239
+ <div style={{ marginTop: 10, paddingTop: 10, borderTop: '1px solid #eee' }}>
240
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
241
+ {eq.ships.map((s, idx) => {
242
+ return (
243
+ <div key={idx} style={{
244
+ display: 'flex',
245
+ alignItems: 'center',
246
+ background: 'rgba(0, 0, 0, 0.2)',
247
+ padding: '4px 8px',
248
+ borderRadius: '4px',
249
+ border: '1px solid rgba(255, 255, 255, 0.1)'
250
+ }}>
251
+ <Avatar mstId={s.providerId} height={20} style={{ marginRight: 5 }} />
252
+ <div style={{ fontSize: '0.9em' }}>
253
+ <span>{s.providerName}</span>
254
+ <Tag minimal={true} style={{ marginLeft: 5 }}>Lv.{s.level}</Tag>
255
+ </div>
256
+ </div>
257
+ )
258
+ })}
259
+ </div>
260
+ </div>
261
+ </Collapse>
262
+ </Card>
263
+ )
264
+ })}
265
+ </div>
266
+ )
267
+ })}
268
+ {sortedTypeIds.length === 0 && <div className="bp3-text-muted" style={{ textAlign: 'center', marginTop: 20 }}>No items match filter.</div>}
269
+ </div>
270
+ </div>
271
+ )
272
+ }
273
+ }
@@ -0,0 +1,145 @@
1
+ import React, { Component } from 'react'
2
+ import { Card, Button, InputGroup, Tag, Collapse } from '@blueprintjs/core'
3
+ import { Avatar } from 'views/components/etc/avatar'
4
+ import { matchesSearch } from '../../lib/search-utils'
5
+
6
+ export default class ShipList extends Component {
7
+ constructor(props) {
8
+ super(props)
9
+ this.state = {
10
+ search: '',
11
+ filterMarked: false
12
+ }
13
+ }
14
+
15
+ render() {
16
+ // Consolidated Data passed from index.es (via equipmentList construction)
17
+ // Or we reconstruct it here?
18
+ // Let's rely on logic similar to index.es:
19
+ // equipmentList contains: { ships: [ { shipId (Base), providerId, providerName, level, ... } ] }
20
+
21
+ const { equipmentList, targets, $ships, wctf } = this.props
22
+ const { search, filterMarked } = this.state
23
+
24
+ // WCTF ships data contains chinese_name
25
+ const wctfShips = (wctf && wctf.ships) || {}
26
+
27
+ const shipMap = {}
28
+
29
+ equipmentList.forEach(eq => {
30
+ const targetCount = targets[eq.id] || 0
31
+ const isTarget = targetCount > 0
32
+
33
+ eq.ships.forEach(s => {
34
+ // s.shipId is BASE ID
35
+ if (!shipMap[s.shipId]) {
36
+ // Get full ship data from master data for enhanced search
37
+ const masterShip = $ships[s.shipId] || {}
38
+ const wctfShip = wctfShips[s.shipId] || {}
39
+
40
+ // WCTF data structure: name is an object with language variants
41
+ const chineseName = wctfShip.name && (wctfShip.name.zh_cn || wctfShip.name.chs || wctfShip.name.chinese)
42
+ const yomiName = wctfShip.name && wctfShip.name.yomi
43
+
44
+ shipMap[s.shipId] = {
45
+ baseId: s.shipId,
46
+ name: s.shipName,
47
+ // Add fields for enhanced search
48
+ api_name: masterShip.api_name,
49
+ chinese_name: chineseName || masterShip.chinese_name,
50
+ yomi: yomiName || masterShip.yomi,
51
+ api_yomi: masterShip.api_yomi,
52
+ filename: wctfShip.filename || masterShip.filename,
53
+ wiki_id: wctfShip.wiki_id || masterShip.wiki_id,
54
+ items: [],
55
+ hasActiveTarget: false
56
+ }
57
+ }
58
+
59
+ shipMap[s.shipId].items.push({
60
+ equipName: eq.name,
61
+ equipId: eq.id,
62
+ level: s.level,
63
+ isTarget: isTarget,
64
+ providerName: s.providerName,
65
+ providerId: s.providerId
66
+ })
67
+
68
+ if (isTarget) shipMap[s.shipId].hasActiveTarget = true
69
+ })
70
+ })
71
+
72
+ let ships = Object.values(shipMap)
73
+
74
+ // Sort items per ship by Level Ascending
75
+ ships.forEach(ship => {
76
+ ship.items.sort((a, b) => a.level - b.level)
77
+ })
78
+
79
+ ships = ships.filter(s => {
80
+ // Enhanced multi-language search: supports Chinese, Japanese, Pinyin, Romaji
81
+ if (search && !matchesSearch(search, s)) return false
82
+ if (filterMarked && !s.hasActiveTarget) return false
83
+ return true
84
+ })
85
+
86
+ ships.sort((a,b) => a.baseId - b.baseId)
87
+
88
+ return (
89
+ <div className="ship-list-container" style={{ display: 'flex', flexDirection: 'column', height: '100%', minHeight: 0 }}>
90
+ <div className="filters" style={{ marginBottom: 10, display: 'flex', gap: 10, flexShrink: 0, padding: 2 }}>
91
+ <InputGroup
92
+ leftIcon="search"
93
+ placeholder="Search ship..."
94
+ value={search}
95
+ onChange={(e) => this.setState({ search: e.target.value })}
96
+ fill={true}
97
+ />
98
+ <Button
99
+ active={filterMarked}
100
+ intent={filterMarked ? "primary" : "none"}
101
+ onClick={() => this.setState({ filterMarked: !filterMarked })}
102
+ >
103
+ Marked Only
104
+ </Button>
105
+ </div>
106
+
107
+ <div className="list-content" style={{ flex: 1, overflowY: 'auto', paddingRight: 5 }}>
108
+ {ships.map(ship => (
109
+ <Card key={ship.baseId} elevation={1} style={{ marginBottom: 8, padding: 10 }}>
110
+ <div style={{ display: 'flex', alignItems: 'center' }}>
111
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', marginRight: 15, width: 130 }}>
112
+ <Avatar mstId={ship.baseId} height={30} />
113
+ <div style={{ fontWeight: 'bold', marginTop: 5, textAlign: 'center', fontSize: '1.1em' }}>{ship.name}</div>
114
+ </div>
115
+ <div style={{ flex: 1 }}>
116
+ {ship.items.map((item, idx) => (
117
+ <div key={idx} style={{
118
+ marginBottom: 4,
119
+ padding: '4px 8px',
120
+ background: item.isTarget ? 'rgba(16, 107, 163, 0.25)' : 'rgba(0, 0, 0, 0.15)',
121
+ borderRadius: 4,
122
+ borderLeft: item.isTarget ? '3px solid #106ba3' : '3px solid transparent',
123
+ display: 'flex',
124
+ justifyContent: 'space-between',
125
+ alignItems: 'center'
126
+ }}>
127
+ <div style={{ display: 'flex', flexDirection: 'column' }}>
128
+ <span style={{ fontWeight: '600' }}>{item.equipName}</span>
129
+ <span className="bp3-text-muted" style={{ fontSize: '0.85em' }}>
130
+ via {item.providerName}
131
+ </span>
132
+ </div>
133
+ <Tag minimal={true} className={item.isTarget ? "bp3-intent-primary" : ""}>Lv.{item.level}</Tag>
134
+ </div>
135
+ ))}
136
+ </div>
137
+ </div>
138
+ </Card>
139
+ ))}
140
+ {ships.length === 0 && <div className="bp3-text-muted" style={{ textAlign: 'center', marginTop: 20 }}>No ships found.</div>}
141
+ </div>
142
+ </div>
143
+ )
144
+ }
145
+ }
package/views/index.es ADDED
@@ -0,0 +1,179 @@
1
+ import React, { Component } from 'react'
2
+ import { connect } from 'react-redux'
3
+ import { Tab, Tabs } from '@blueprintjs/core'
4
+ import { addTarget, removeTarget } from '../redux/actions'
5
+ import { targetsSelector, masterShipsSelector, masterEquipmentsSelector, masterShipTypesSelector, masterEquipTypesSelector, wctfDataSelector, userEquipsSelector, userShipsSelector, constSelector } from '../redux/selectors'
6
+ import { getFarmingMap } from '../lib/data-processor'
7
+
8
+ import EquipmentList from './components/EquipmentList'
9
+ import ShipList from './components/ShipList'
10
+
11
+ // Main UI
12
+ class FarmingAssistant extends Component {
13
+ constructor(props) {
14
+ super(props)
15
+ this.state = {
16
+ activeTab: 'equipment',
17
+ }
18
+ }
19
+
20
+ handleTabChange = (newTabId) => {
21
+ this.setState({ activeTab: newTabId })
22
+ }
23
+
24
+ render() {
25
+ const {
26
+ targets,
27
+ addTarget,
28
+ removeTarget,
29
+ $ships,
30
+ $equipments,
31
+ $shipTypes,
32
+ $equipTypes,
33
+ wctf,
34
+ userEquips,
35
+ userShips
36
+ } = this.props
37
+
38
+ // 1. Generate Farming Map from WCTF (Consolidated by Base ID)
39
+ const farmingMap = getFarmingMap(wctf)
40
+
41
+ // 2. Convert Map to List for UI
42
+ const equipmentMap = {}
43
+
44
+ // WCTF items data contains chinese_name
45
+ const wctfItems = (wctf && wctf.items) || {}
46
+
47
+ Object.keys(farmingMap).forEach(baseShipIdStr => {
48
+ const baseShipId = parseInt(baseShipIdStr)
49
+ const info = farmingMap[baseShipIdStr]
50
+
51
+ // Name Fix: Lookup Base Ship Name
52
+ const baseShipMaster = $ships[baseShipId] || {}
53
+ const baseShipName = baseShipMaster.api_name || `Ship#${baseShipId}`
54
+
55
+ info.provides.forEach(p => {
56
+ const equipId = p.equipId
57
+
58
+ if (!equipmentMap[equipId]) {
59
+ const masterEquip = $equipments[equipId] || {}
60
+ const wctfItem = wctfItems[equipId] || {}
61
+ const typeId = (masterEquip.api_type && masterEquip.api_type[2]) || 0
62
+
63
+ // WCTF data structure: name is an object with language variants
64
+ const chineseName = wctfItem.name && (wctfItem.name.zh_cn || wctfItem.name.chs || wctfItem.name.chinese)
65
+ const yomiName = wctfItem.name && wctfItem.name.yomi
66
+
67
+ equipmentMap[equipId] = {
68
+ id: equipId,
69
+ name: masterEquip.api_name || `Equip#${equipId}`,
70
+ // Add fields for enhanced search
71
+ api_name: masterEquip.api_name,
72
+ chinese_name: chineseName || masterEquip.chinese_name,
73
+ yomi: yomiName,
74
+ filename: wctfItem.filename,
75
+ wiki_id: wctfItem.wiki_id,
76
+ iconId: (masterEquip.api_type && masterEquip.api_type[3]) || 0,
77
+ typeName: ($equipTypes[typeId] || {}).api_name || 'Unknown',
78
+ typeId: typeId,
79
+ ships: []
80
+ }
81
+ }
82
+
83
+ // Get Provider (Evolution) Name
84
+ const providerMaster = $ships[p.providerId] || {}
85
+ const providerName = providerMaster.api_name || `Form#${p.providerId}`
86
+
87
+ equipmentMap[equipId].ships.push({
88
+ shipId: baseShipId, // Use BASE ID for grouping
89
+ shipName: baseShipName,
90
+ providerId: p.providerId,
91
+ providerName: providerName,
92
+ level: p.level,
93
+ remodel: true
94
+ })
95
+
96
+ // Sort by Level Ascending (Low -> High)
97
+ equipmentMap[equipId].ships.sort((a, b) => a.level - b.level)
98
+ })
99
+ })
100
+
101
+ const equipmentList = Object.values(equipmentMap)
102
+
103
+ return (
104
+ <div className="farming-assistant-root" style={{ height: '100%', display: 'flex', flexDirection: 'column', padding: '0 10px' }}>
105
+ <style>{`
106
+ .farming-tabs {
107
+ height: 100%;
108
+ display: flex;
109
+ flex-direction: column;
110
+ }
111
+ .farming-tabs .bp3-tab-list {
112
+ flex-shrink: 0;
113
+ }
114
+ .farming-tabs .bp3-tab-panel {
115
+ flex: 1;
116
+ display: flex;
117
+ flex-direction: column;
118
+ min-height: 0;
119
+ margin-top: 10px;
120
+ overflow: hidden;
121
+ position: relative; /* Anchor for absolute child */
122
+ }
123
+ `}</style>
124
+ <div style={{ flex: 1, minHeight: 0 }}>
125
+ <Tabs
126
+ id="farming-tabs"
127
+ className="farming-tabs"
128
+ onChange={this.handleTabChange}
129
+ selectedTabId={this.state.activeTab}
130
+ animate={true}
131
+ renderActiveTabPanelOnly={true}
132
+ >
133
+ <Tab id="equipment" title="Equipments" panel={
134
+ <div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
135
+ <EquipmentList
136
+ equipments={equipmentList}
137
+ targets={targets}
138
+ onAdd={addTarget}
139
+ onRemove={removeTarget}
140
+ userEquips={userEquips}
141
+ userShips={userShips}
142
+ farmingMap={farmingMap}
143
+ $equipTypes={$equipTypes}
144
+ $ships={$ships}
145
+ wctf={wctf}
146
+ />
147
+ </div>
148
+ } />
149
+ <Tab id="ships" title="Ships" panel={
150
+ <div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
151
+ <ShipList
152
+ equipmentList={equipmentList}
153
+ targets={targets}
154
+ $ships={$ships}
155
+ wctf={wctf}
156
+ />
157
+ </div>
158
+ } />
159
+ </Tabs>
160
+ </div>
161
+ </div>
162
+ )
163
+ }
164
+ }
165
+
166
+ export const reactClass = connect(
167
+ (state) => ({
168
+ const: constSelector(state),
169
+ targets: targetsSelector(state),
170
+ $ships: masterShipsSelector(state),
171
+ $equipments: masterEquipmentsSelector(state),
172
+ $shipTypes: masterShipTypesSelector(state),
173
+ $equipTypes: masterEquipTypesSelector(state),
174
+ wctf: wctfDataSelector(state),
175
+ userEquips: userEquipsSelector(state),
176
+ userShips: userShipsSelector(state),
177
+ }),
178
+ { addTarget, removeTarget }
179
+ )(FarmingAssistant)