返回提交历史
Modified
src/controllers/custom/getItemListsController.ts
+9
-2
Modified
static/webui/index.html
+80
-35
Modified
static/webui/script.js
+191
-20
Modified
static/webui/style.css
+2
-3
XFEstudio/XFESpaceNinjaServer
feat(webui): add Mods card (#252)
f83f80c9
代码差异
4 个文件
+282
-60
@@ -1,16 +1,19 @@
1
1
import { RequestHandler } from "express";
2
2
import { MinItem, warframes, weapons, items } from "@/src/services/itemDataService";
3
import badItems from "@/static/json/exclude-mods.json";
3
4
4
5
interface ListedItem {
5
6
uniqueName: string;
6
7
name: string;
8
fusionLimit?: number;
7
9
}
8
10
9
11
function reduceItems(items: MinItem[]): ListedItem[] {
10
12
return items.map((item: MinItem): ListedItem => {
11
13
return {
12
14
uniqueName: item.uniqueName,
13
name: item.name
15
name: item.name,
16
fusionLimit: (item as any).fusionLimit
14
17
};
15
18
});
16
19
}
@@ -19,7 +22,11 @@ const getItemListsController: RequestHandler = (_req, res) => {
19
22
res.json({
20
23
warframes: reduceItems(warframes),
21
24
weapons: reduceItems(weapons.filter(item => item.productCategory != "OperatorAmps")),
22
miscitems: reduceItems(items.filter(item => item.category == "Misc" || item.category == "Resources"))
25
miscitems: reduceItems(
26
items.filter(item => item.category == "Misc" || item.category == "Resources" || item.category == "Fish")
27
),
28
mods: reduceItems(items.filter(item => item.category == "Mods" || item.category == "Arcanes")),
29
badItems
23
30
});
24
31
};
25
32
@@ -45,14 +45,30 @@
45
45
></button>
46
46
</div>
47
47
<div class="offcanvas-body">
48
<ul>
49
<li>
50
<a href="/webui/inventory" data-bs-dismiss="offcanvas" data-bs-target="#sidebar"
51
>Inventory</a
52
>
53
</li>
54
<li><a href="/webui/mods" data-bs-dismiss="offcanvas" data-bs-target="#sidebar">Mods</a></li>
55
</ul>
48
<div class="navbar p-0">
49
<ul class="navbar-nav justify-content-end">
50
<li class="nav-item">
51
<a
52
class="nav-link"
53
href="/webui/inventory"
54
data-bs-dismiss="offcanvas"
55
data-bs-target="#sidebar"
56
>
57
Inventory
58
</a>
59
</li>
60
<li class="nav-item">
61
<a
62
class="nav-link"
63
href="/webui/mods"
64
data-bs-dismiss="offcanvas"
65
data-bs-target="#sidebar"
66
>
67
Mods
68
</a>
69
</li>
70
</ul>
71
</div>
56
72
</div>
57
73
</div>
58
74
<div>
@@ -117,33 +133,61 @@
117
133
</div>
118
134
</div>
119
135
<div data-route="/webui/mods" data-title="Mods | OpenWF WebUI">
120
<div class="card mb-4">
121
<h5 class="card-header">Add Riven</h5>
122
<form class="card-body" onsubmit="doAcquireRiven();return false;">
123
<select class="form-control mb-3" id="addriven-type">
124
<option value="LotusArchgunRandomModRare">LotusArchgunRandomModRare</option>
125
<option value="LotusModularMeleeRandomModRare">LotusModularMeleeRandomModRare</option>
126
<option value="LotusModularPistolRandomModRare">LotusModularPistolRandomModRare</option>
127
<option value="LotusPistolRandomModRare">LotusPistolRandomModRare</option>
128
<option value="LotusRifleRandomModRare" selected>LotusRifleRandomModRare</option>
129
<option value="LotusShotgunRandomModRare">LotusShotgunRandomModRare</option>
130
<option value="PlayerMeleeWeaponRandomModRare">PlayerMeleeWeaponRandomModRare</option>
131
</select>
132
<textarea
133
id="addriven-fingerprint"
134
class="form-control mb-3"
135
placeholder="Fingerprint"
136
></textarea>
137
<button class="btn btn-primary" style="margin-right: 5px" type="submit">Add</button>
138
<a href="https://riven.builds.wf/" target="_blank">Need help with the fingerprint?</a>
139
</form>
140
</div>
141
<div class="card mb-4 col-xxl-6">
142
<h5 class="card-header">Rivens</h5>
143
<div class="card-body">
144
<table class="table table-hover w-100">
145
<tbody id="riven-list"></tbody>
146
</table>
136
<div class="row">
137
<div class="col-xxl-6">
138
<div class="card mb-4">
139
<h5 class="card-header">Add Riven</h5>
140
<form class="card-body" onsubmit="doAcquireRiven();return false;">
141
<select class="form-control mb-3" id="addriven-type">
142
<option value="LotusArchgunRandomModRare">LotusArchgunRandomModRare</option>
143
<option value="LotusModularMeleeRandomModRare">
144
LotusModularMeleeRandomModRare
145
</option>
146
<option value="LotusModularPistolRandomModRare">
147
LotusModularPistolRandomModRare
148
</option>
149
<option value="LotusPistolRandomModRare">LotusPistolRandomModRare</option>
150
<option value="LotusRifleRandomModRare" selected>
151
LotusRifleRandomModRare
152
</option>
153
<option value="LotusShotgunRandomModRare">LotusShotgunRandomModRare</option>
154
<option value="PlayerMeleeWeaponRandomModRare">
155
PlayerMeleeWeaponRandomModRare
156
</option>
157
</select>
158
<textarea
159
id="addriven-fingerprint"
160
class="form-control mb-3"
161
placeholder="Fingerprint"
162
></textarea>
163
<button class="btn btn-primary" style="margin-right: 5px" type="submit">Add</button>
164
<a href="https://riven.builds.wf/" target="_blank">
165
Need help with the fingerprint?
166
</a>
167
</form>
168
</div>
169
<div class="card mb-4">
170
<h5 class="card-header">Rivens</h5>
171
<div class="card-body">
172
<table class="table table-hover w-100">
173
<tbody id="riven-list"></tbody>
174
</table>
175
</div>
176
</div>
177
</div>
178
<div class="col-xxl-6">
179
<div class="card mb-4">
180
<h5 class="card-header">Mods</h5>
181
<div class="card-body">
182
<table class="table table-hover w-100">
183
<tbody id="mods-list"></tbody>
184
</table>
185
<form class="input-group" onsubmit="doAcquireMod();return false;">
186
<input class="form-control" id="mod-to-acquire" list="datalist-mods" />
187
<button class="btn btn-primary" type="submit">Add</button>
188
</form>
189
</div>
190
</div>
147
191
</div>
148
192
</div>
149
193
</div>
@@ -152,6 +196,7 @@
152
196
<datalist id="datalist-warframes"></datalist>
153
197
<datalist id="datalist-weapons"></datalist>
154
198
<datalist id="datalist-miscitems"></datalist>
199
<datalist id="datalist-mods"></datalist>
155
200
<script src="libs/jquery-3.6.0.min.js"></script>
156
201
<script src="libs/whirlpool-js.min.js"></script>
157
202
<script src="libs/single.js"></script>
@@ -76,33 +76,47 @@ single.on("route_load", function (event) {
76
76
} else {
77
77
$("body").removeClass("logged-in");
78
78
}
79
80
$(".nav-link").removeClass("active");
81
const navLink = document.querySelector(".nav-link[href='" + event.route.paths[0] + "']");
82
if (navLink) {
83
navLink.classList.add("active");
84
}
79
85
});
80
86
81
87
window.itemListPromise = new Promise(resolve => {
82
88
const req = $.get("/custom/getItemLists");
83
89
req.done(data => {
84
const itemMap = {};
90
const itemMap = {
91
// Generics for rivens
92
"/Lotus/Weapons/Tenno/Archwing/Primary/ArchGun": { name: "Archgun" },
93
"/Lotus/Weapons/Tenno/Melee/PlayerMeleeWeapon": { name: "Melee" },
94
"/Lotus/Weapons/Tenno/Pistol/LotusPistol": { name: "Pistol" },
95
"/Lotus/Weapons/Tenno/Rifle/LotusRifle": { name: "Rifle" },
96
"/Lotus/Weapons/Tenno/Shotgun/LotusShotgun": { name: "Shotgun" },
97
// Missing in data sources
98
"/Lotus/Upgrades/CosmeticEnhancers/Peculiars/CyoteMod": { name: "Traumatic Peculiar" },
99
"/Lotus/Weapons/Tenno/Grimoire/TnGrimoire": { name: "Grimoire" }
100
};
85
101
for (const [type, items] of Object.entries(data)) {
86
items.forEach(item => {
87
const option = document.createElement("option");
88
option.setAttribute("data-key", item.uniqueName);
89
option.value = item.name;
90
document.getElementById("datalist-" + type).appendChild(option);
91
itemMap[item.uniqueName] = { ...item, type };
92
});
102
if (type != "badItems") {
103
items.forEach(item => {
104
if (item.uniqueName in data.badItems) {
105
item.name += " (Imposter)";
106
} else if (item.uniqueName.substr(0, 18) != "/Lotus/Types/Game/") {
107
const option = document.createElement("option");
108
option.setAttribute("data-key", item.uniqueName);
109
option.value = item.name;
110
document.getElementById("datalist-" + type).appendChild(option);
111
}
112
itemMap[item.uniqueName] = { ...item, type };
113
});
114
}
93
115
}
94
116
resolve(itemMap);
95
117
});
96
118
});
97
119
98
const rivenGenericCompatNames = {
99
"/Lotus/Weapons/Tenno/Archwing/Primary/ArchGun": "Archgun",
100
"/Lotus/Weapons/Tenno/Melee/PlayerMeleeWeapon": "Melee",
101
"/Lotus/Weapons/Tenno/Pistol/LotusPistol": "Pistol",
102
"/Lotus/Weapons/Tenno/Rifle/LotusRifle": "Rifle",
103
"/Lotus/Weapons/Tenno/Shotgun/LotusShotgun": "Shotgun"
104
};
105
106
120
function updateInventory() {
107
121
const req = $.get("/api/inventory.php?" + window.authz);
108
122
req.done(data => {
@@ -190,6 +204,7 @@ function updateInventory() {
190
204
});
191
205
192
206
document.getElementById("riven-list").innerHTML = "";
207
document.getElementById("mods-list").innerHTML = "";
193
208
data.Upgrades.forEach(item => {
194
209
if (item.ItemType.substr(0, 32) == "/Lotus/Upgrades/Mods/Randomized/") {
195
210
const rivenType = item.ItemType.substr(32);
@@ -198,10 +213,7 @@ function updateInventory() {
198
213
const tr = document.createElement("tr");
199
214
{
200
215
const td = document.createElement("td");
201
td.textContent =
202
itemMap[fingerprint.compat]?.name ??
203
rivenGenericCompatNames[fingerprint.compat] ??
204
fingerprint.compat;
216
td.textContent = itemMap[fingerprint.compat]?.name ?? fingerprint.compat;
205
217
td.textContent += " " + RivenParser.parseRiven(rivenType, fingerprint, 1).name;
206
218
td.innerHTML += " <span title='Number of buffs'>▲ " + fingerprint.buffs.length + "</span>";
207
219
td.innerHTML += " <span title='Number of curses'>▼ " + fingerprint.curses.length + "</span>";
@@ -245,6 +257,92 @@ function updateInventory() {
245
257
tr.appendChild(td);
246
258
}
247
259
document.getElementById("riven-list").appendChild(tr);
260
} else {
261
const tr = document.createElement("tr");
262
const rank = parseInt(JSON.parse(item.UpgradeFingerprint).lvl);
263
const maxRank = itemMap[item.ItemType]?.fusionLimit ?? 5;
264
{
265
const td = document.createElement("td");
266
td.textContent = itemMap[item.ItemType]?.name ?? item.ItemType;
267
td.innerHTML += " <span title='Rank'>★ " + rank + "/" + maxRank + "</span>";
268
tr.appendChild(td);
269
}
270
{
271
const td = document.createElement("td");
272
td.classList = "text-end";
273
if (rank < maxRank) {
274
const a = document.createElement("a");
275
a.href = "#";
276
a.onclick = function (event) {
277
event.preventDefault();
278
setFingerprint(item.ItemType, item.ItemId, { lvl: maxRank });
279
};
280
a.textContent = "Max Rank";
281
td.appendChild(a);
282
283
const span = document.createElement("span");
284
span.innerHTML = " · ";
285
td.appendChild(span);
286
}
287
{
288
const a = document.createElement("a");
289
a.href = "#";
290
a.onclick = function (event) {
291
event.preventDefault();
292
disposeOfGear("Upgrades", item.ItemId.$oid);
293
};
294
a.textContent = "Remove";
295
td.appendChild(a);
296
}
297
tr.appendChild(td);
298
}
299
document.getElementById("mods-list").appendChild(tr);
300
}
301
});
302
data.RawUpgrades.forEach(item => {
303
if (item.ItemCount > 0) {
304
const maxRank = itemMap[item.ItemType]?.fusionLimit ?? 5;
305
const tr = document.createElement("tr");
306
{
307
const td = document.createElement("td");
308
td.textContent = itemMap[item.ItemType]?.name ?? item.ItemType;
309
td.innerHTML += " <span title='Rank'>★ 0/" + maxRank + "</span>";
310
if (item.ItemCount > 1) {
311
td.innerHTML += " <span title='Count'>🗍 " + parseInt(item.ItemCount) + "</span>";
312
}
313
tr.appendChild(td);
314
}
315
{
316
const td = document.createElement("td");
317
td.classList = "text-end";
318
{
319
const a = document.createElement("a");
320
a.href = "#";
321
a.onclick = function (event) {
322
event.preventDefault();
323
setFingerprint(item.ItemType, item.LastAdded, { lvl: maxRank });
324
};
325
a.textContent = "Max Rank";
326
td.appendChild(a);
327
}
328
{
329
const span = document.createElement("span");
330
span.innerHTML = " · ";
331
td.appendChild(span);
332
}
333
{
334
const a = document.createElement("a");
335
a.href = "#";
336
a.onclick = function (event) {
337
event.preventDefault();
338
disposeOfItems("Upgrades", item.ItemType, item.ItemCount);
339
};
340
a.textContent = "Remove";
341
td.appendChild(a);
342
}
343
tr.appendChild(td);
344
}
345
document.getElementById("mods-list").appendChild(tr);
248
346
}
249
347
});
250
348
});
@@ -352,6 +450,29 @@ function disposeOfGear(category, oid) {
352
450
});
353
451
}
354
452
453
function disposeOfItems(category, type, count) {
454
const data = {
455
SellCurrency: "SC_RegularCredits",
456
SellPrice: 0,
457
Items: {}
458
};
459
data.Items[category] = [
460
{
461
String: type,
462
Count: count
463
}
464
];
465
revalidateAuthz(() => {
466
$.post({
467
url: "/api/sell.php?" + window.authz,
468
contentType: "text/plain",
469
data: JSON.stringify(data)
470
}).done(function () {
471
updateInventory();
472
});
473
});
474
}
475
355
476
function doAcquireMiscItems() {
356
477
const uniqueName = getKey(document.getElementById("miscitem-type"));
357
478
if (!uniqueName) {
@@ -445,3 +566,53 @@ function doAcquireRiven() {
445
566
$("#addriven-fingerprint").on("input", () => {
446
567
$("#addriven-fingerprint").removeClass("is-invalid");
447
568
});
569
570
function setFingerprint(ItemType, ItemId, fingerprint) {
571
revalidateAuthz(() => {
572
$.post({
573
url: "/api/artifacts.php?" + window.authz,
574
contentType: "text/plain",
575
data: JSON.stringify({
576
Upgrade: {
577
ItemType,
578
ItemId,
579
UpgradeFingerprint: JSON.stringify(fingerprint)
580
},
581
LevelDiff: 0,
582
Cost: 0,
583
FusionPointCost: 0
584
})
585
}).done(function () {
586
updateInventory();
587
});
588
});
589
}
590
591
function doAcquireMod() {
592
const uniqueName = getKey(document.getElementById("mod-to-acquire"));
593
if (!uniqueName) {
594
$("#mod-to-acquire").addClass("is-invalid").focus();
595
return;
596
}
597
revalidateAuthz(() => {
598
$.post({
599
url: "/api/missionInventoryUpdate.php?" + window.authz,
600
contentType: "text/plain",
601
data: JSON.stringify({
602
RawUpgrades: [
603
{
604
ItemType: uniqueName,
605
ItemCount: 1
606
}
607
]
608
})
609
}).done(function () {
610
document.getElementById("mod-to-acquire").value = "";
611
updateInventory();
612
});
613
});
614
}
615
616
$("#mod-to-acquire").on("input", () => {
617
$("#mod-to-acquire").removeClass("is-invalid");
618
});
@@ -1,14 +1,13 @@
1
1
@media (min-width: 992px) {
2
2
body.logged-in #main-view {
3
display: grid;
4
grid-template-columns: 1fr 8fr;
5
gap: 1.5rem;
3
display: flex;
6
4
}
7
5
8
6
body.logged-in #sidebar {
9
7
position: sticky;
10
8
top: 5rem;
11
9
height: 100px;
10
margin-right: 3rem;
12
11
}
13
12
14
13
body:not(.logged-in) #sidebar {