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.
- package/README.md +206 -0
- package/assets/ships.json +180 -0
- package/index.es +107 -0
- package/lib/data-processor.es +168 -0
- package/lib/search-utils.es +141 -0
- package/lib/utils.es +106 -0
- package/package.json +42 -0
- package/redux/actions.es +25 -0
- package/redux/index.es +3 -0
- package/redux/reducer.es +45 -0
- package/redux/selectors.es +61 -0
- package/views/components/EquipmentList.es +273 -0
- package/views/components/ShipList.es +145 -0
- package/views/index.es +179 -0
|
@@ -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)
|