Compare commits

..

143 Commits

Author SHA1 Message Date
62a6042c9c fix(webui): save ProgressOverride value (#2638)
Reviewed-on: OpenWF/SpaceNinjaServer#2638
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-08-16 05:42:09 -07:00
e8d4d84d6e chore(webui): update uk (#2639)
Reviewed-on: OpenWF/SpaceNinjaServer#2639
Co-authored-by: LoseFace <loseface@noreply.localhost>
Co-committed-by: LoseFace <loseface@noreply.localhost>
2025-08-16 05:41:12 -07:00
62881aaa36 feat: articula customizations (#2636)
Reviewed-on: OpenWF/SpaceNinjaServer#2636
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-16 05:40:16 -07:00
df316e3a7a feat: conclave challenges rotation (#2635)
Re #1192

Reviewed-on: OpenWF/SpaceNinjaServer#2635
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-08-16 05:39:23 -07:00
264e9cfc98 fix: use flat rush cost at <50% progress (#2634)
otherwise the cost would be increased instead of decreased

Reviewed-on: OpenWF/SpaceNinjaServer#2634
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-16 05:39:00 -07:00
5d5554a80e feat: h-09 apex turret sumdali reward (#2633)
Closes #2630

Reviewed-on: OpenWF/SpaceNinjaServer#2633
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-16 05:38:48 -07:00
da14a4081b chore: put reward year into goal _id (#2626)
Closes #2623

Reviewed-on: OpenWF/SpaceNinjaServer#2626
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-08-15 21:02:15 -07:00
b0b68f474a feat: getShip import (#2627)
Re #2592
Unsure about import note, is it okay that we leave the API path?

Reviewed-on: OpenWF/SpaceNinjaServer#2627
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-08-15 15:13:34 -07:00
ab214df1a8 chore(webui): update ru & uk (#2632)
Updated and improved some translations

Reviewed-on: OpenWF/SpaceNinjaServer#2632
Co-authored-by: LoseFace <loseface@noreply.localhost>
Co-committed-by: LoseFace <loseface@noreply.localhost>
2025-08-15 14:57:04 -07:00
9f8105d7f1 fix: extractor drone reward amounts (#2629)
Fixes #2628

Reviewed-on: OpenWF/SpaceNinjaServer#2629
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: VampireKitten <dynamightkobold@gmail.com>
Co-committed-by: VampireKitten <dynamightkobold@gmail.com>
2025-08-15 14:56:39 -07:00
c47a29ec96 chore: note 2025-08-15 18:15:10 +02:00
6d727c50f4 chore: handle addItem of GhoulFragmentRewards (#2625)
Closes #2624

Reviewed-on: OpenWF/SpaceNinjaServer#2625
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-15 08:15:51 -07:00
bf04755c36 feat: belly of the beast / eight claw (#2621)
Re #1103

Reviewed-on: OpenWF/SpaceNinjaServer#2621
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-08-15 08:14:36 -07:00
e345fc35b6 chore(webui): update es (#2622)
Reviewed-on: OpenWF/SpaceNinjaServer#2622
Co-authored-by: Slayer55555 <slayer55555@noreply.localhost>
Co-committed-by: Slayer55555 <slayer55555@noreply.localhost>
2025-08-14 08:38:43 -07:00
f5335704b4 chore: make 'infinite' cheats per-account toggles (#2619)
Re #2361

Reviewed-on: OpenWF/SpaceNinjaServer#2619
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-14 07:58:41 -07:00
79c5f7a67a chore: fix cyclic include for slotPurchaseNameToSlotName (#2618)
Reviewed-on: OpenWF/SpaceNinjaServer#2618
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-14 07:56:45 -07:00
e97b107853 feat: nemesis convert message (#2616)
Closes #2614

Reviewed-on: OpenWF/SpaceNinjaServer#2616
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-14 07:56:29 -07:00
7bc5065251 chore(webui): update uk (#2617)
Reviewed-on: OpenWF/SpaceNinjaServer#2617
Co-authored-by: LoseFace <loseface@noreply.localhost>
Co-committed-by: LoseFace <loseface@noreply.localhost>
2025-08-13 13:03:20 -07:00
3194a693b3 fix: hardcode rotation A for non-endless railjack missions (#2613)
Closes #2612

Reviewed-on: OpenWF/SpaceNinjaServer#2613
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-13 07:13:41 -07:00
261dbd5fdf feat: railjack abandoned caches (#2611)
Closes #2602

Reviewed-on: OpenWF/SpaceNinjaServer#2611
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: VampireKitten <dynamightkobold@gmail.com>
Co-committed-by: VampireKitten <dynamightkobold@gmail.com>
2025-08-13 07:13:26 -07:00
fd2ec696a0 feat: tactical alerts (#2607)
Includes all `Tactical Alerts` since Star Chart 3.0 with exception:
`Snowday Showdown`
`Wolf Hunt (2019)` (couldn't find corresponded `EventNode` for that)
`Void Corruption` (that's goes into `Alerts`)
All `Warframe's Anniversary`

Re #1103

Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Reviewed-on: OpenWF/SpaceNinjaServer#2607
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-08-13 07:13:05 -07:00
9cc0c76ef5 chore(webui): omit conclave from supported syndicates (#2608)
Reviewed-on: OpenWF/SpaceNinjaServer#2608
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-12 06:32:41 -07:00
2a4488d1dd chore(webui): update fr (#2609)
Reviewed-on: OpenWF/SpaceNinjaServer#2609
Co-authored-by: Vitruvio <vitruvio@noreply.localhost>
Co-committed-by: Vitruvio <vitruvio@noreply.localhost>
2025-08-12 06:32:27 -07:00
2e1326cde8 chore: update PE+ (#2606)
Reviewed-on: OpenWF/SpaceNinjaServer#2606
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-11 08:09:30 -07:00
70be467cbf feat: disruption rewards (#2605)
Closes #2599

Reviewed-on: OpenWF/SpaceNinjaServer#2605
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-11 08:09:15 -07:00
fac3ec01c6 chore: improve structuring of mission response types (#2604)
Reviewed-on: OpenWF/SpaceNinjaServer#2604
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-11 08:09:08 -07:00
ebdca760e6 chore: simplify syncing of challenge 'Completed' field (#2603)
Challenges are mostly client-authoritative, so narrow the special-casing to "challengeRewards".

Reviewed-on: OpenWF/SpaceNinjaServer#2603
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-11 08:08:47 -07:00
51c0ddda38 feat(goals): cetus events (#2598)
Includes `Plague Star` and `Ghoul Purge`.
Translation for webUI taken from game files.
Re #1103

Reviewed-on: OpenWF/SpaceNinjaServer#2598
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-08-11 08:08:40 -07:00
9129bdb5fc chore(webui): update zh (#2601)
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Reviewed-on: OpenWF/SpaceNinjaServer#2601
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: qingchun <qingchun@noreply.localhost>
Co-committed-by: qingchun <qingchun@noreply.localhost>
2025-08-10 16:53:03 -07:00
a4922d4c35 chore: improve handling of RJ interstitial missionInventoryUpdate (#2600)
InventoryJson should only be returned when going back to dojo, in which case RJ is also not present in the request anymore.

Reviewed-on: OpenWF/SpaceNinjaServer#2600
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-09 03:38:10 -07:00
679752633a feat: recover nightwave challenges (#2593)
Closes #1534

Reviewed-on: OpenWF/SpaceNinjaServer#2593
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-08-08 04:21:18 -07:00
67b5890f39 feat(webui): ukrainian translation by LoseFace (#2596)
Reviewed-on: OpenWF/SpaceNinjaServer#2596
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-07 12:31:38 -07:00
5d54e79e5d chore(webui): russian translation update by LoseFace (#2595)
Reviewed-on: OpenWF/SpaceNinjaServer#2595
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-07 12:31:28 -07:00
4606f28a58 fix(webui): incorect values for ability override request (#2591)
Reviewed-on: OpenWF/SpaceNinjaServer#2591
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-08-07 03:59:25 -07:00
a2d383ee3c fix: ignore rewardQualifications for non-endless mission types (#2590)
Closes #2586

Reviewed-on: OpenWF/SpaceNinjaServer#2590
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-06 04:01:01 -07:00
834b7a8196 fix(webui): email address not being lowercased (#2589)
Regression introduced by 2fa6dcc7edb34c9382c31739d8b61a84803d69c2, which threw out the change introduced by 1fd801403fc8d24851e46477258759d0149eb76f.

Reviewed-on: OpenWF/SpaceNinjaServer#2589
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-06 04:00:43 -07:00
4a2d863c9c chore(webui): update to Spanish translation (#2588)
Reviewed-on: OpenWF/SpaceNinjaServer#2588
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-08-05 10:18:36 -07:00
9f0cd91105 chore(webui): update German translation (#2587)
I need to double check some other time if it's also called "TennoLive" in German wf, once I'm home again. Should be prob good enough for now...

Reviewed-on: OpenWF/SpaceNinjaServer#2587
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-08-05 09:47:25 -07:00
ebfef52fb1 fix(webui): proper PvPVariant check (#2585)
Closes #2584

Reviewed-on: OpenWF/SpaceNinjaServer#2585
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-08-04 05:18:45 -07:00
dd7bacd22e chore: update PE+ (#2583)
Reviewed-on: OpenWF/SpaceNinjaServer#2583
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-03 15:37:06 -07:00
c00967931e fix(webui): add k-drive (#2581)
Closes #2580

Reviewed-on: OpenWF/SpaceNinjaServer#2581
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-02 04:57:16 -07:00
b15a635e11 fix(webui): exclude zaw strike pvp variants (#2579)
Reviewed-on: OpenWF/SpaceNinjaServer#2579
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-02 04:57:08 -07:00
7e618539fa fix(webui): explicitly specify websocket protocol (#2578)
apparently some browsers (e.g. 2 year old chrome) need this to establish a connection. can't hurt, anyway.

Reviewed-on: OpenWF/SpaceNinjaServer#2578
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-08-01 03:47:14 -07:00
a29398fae6 chore(webui): update Chinese translation (#2577)
Reviewed-on: OpenWF/SpaceNinjaServer#2577
Co-authored-by: Corvus <corvus@noreply.localhost>
Co-committed-by: Corvus <corvus@noreply.localhost>
2025-07-31 07:46:01 -07:00
601091f1c0 chore(webui): clarify that eidolon override also takes effect on deimos (#2576)
This is an update in the English translation only.

Reviewed-on: OpenWF/SpaceNinjaServer#2576
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-31 02:28:03 -07:00
f561884f2c feat: worldState.baroTennoConRelay config (#2574)
Closes #2531

Reviewed-on: OpenWF/SpaceNinjaServer#2574
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-31 02:27:38 -07:00
6e1cb0c9f9 chore(webui): update Chinese translation (#2575)
Reviewed-on: OpenWF/SpaceNinjaServer#2575
Co-authored-by: Corvus <corvus@noreply.localhost>
Co-committed-by: Corvus <corvus@noreply.localhost>
2025-07-31 02:27:26 -07:00
9286627668 feat: star days rotation (#2573)
Closes #2567

Reviewed-on: OpenWF/SpaceNinjaServer#2573
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-30 05:44:42 -07:00
f94f2005d3 fix: script error with baroFullyStocked 2025-07-30 13:45:00 +02:00
9901b7af54 chore: rename config.json.example to config-vanilla.json (#2570)
Changing file extensions can be a bit of a chore on stock Windows, so this should simplify matters. Another bonus is that the "vanilla" clarifies the general guideline for how the defaults are configured.

Reviewed-on: OpenWF/SpaceNinjaServer#2570
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-30 04:01:02 -07:00
2fa846f465 chore(webui): update Chinese translation (#2572)
Reviewed-on: OpenWF/SpaceNinjaServer#2572
Co-authored-by: Corvus <corvus@noreply.localhost>
Co-committed-by: Corvus <corvus@noreply.localhost>
2025-07-30 03:51:42 -07:00
541ec3d702 feat: claiming of tennolive relay's secret (#2569)
Reviewed-on: OpenWF/SpaceNinjaServer#2569
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-30 01:51:07 -07:00
0a28eab65d feat: worldState.tennoLiveRelay config (#2568)
Re #2531

Reviewed-on: OpenWF/SpaceNinjaServer#2568
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-30 01:50:43 -07:00
8e639a16bd feat: initial protovyre/evolving cosmetics (#2566)
Basic handling of sending the challenge rewards to the inbox upon completion.

Re #2485. Still missing handling for the Protovyre armor pieces which require killing sentients.

Reviewed-on: OpenWF/SpaceNinjaServer#2566
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-30 01:50:23 -07:00
522924a823 chore: remove empty ModularParts arrays from equipment (#2565)
Reviewed-on: OpenWF/SpaceNinjaServer#2565
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-30 01:49:55 -07:00
48e3f324e2 chore: log when worldState time is behind real time + make sure client knows fissures are active (#2562)
Reviewed-on: OpenWF/SpaceNinjaServer#2562
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-30 01:49:34 -07:00
8f77c722cb chore(webui): update German translation (#2564)
Reviewed-on: OpenWF/SpaceNinjaServer#2564
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-07-29 07:34:04 -07:00
e7287933b5 chore(webui): update Chinese translation (#2563)
Reviewed-on: OpenWF/SpaceNinjaServer#2563
Co-authored-by: Corvus <corvus@noreply.localhost>
Co-committed-by: Corvus <corvus@noreply.localhost>
2025-07-29 07:33:57 -07:00
b21bca7a6d fix: AffiliationChanges disapears from EOM screen when bounty stage is completed (#2560)
Reviewed-on: OpenWF/SpaceNinjaServer#2560
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-07-29 00:31:48 -07:00
d30d450311 chore: add rewards for NewbieJob (#2559)
Closes #2536

Reviewed-on: OpenWF/SpaceNinjaServer#2559
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-07-29 00:31:37 -07:00
b62e326920 feat(webui): ability overrides (#2558)
Closes #851

Reviewed-on: OpenWF/SpaceNinjaServer#2558
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-07-29 00:31:29 -07:00
8b4bc114f6 chore: add logging for bounty medallion rewards (#2557)
Reviewed-on: OpenWF/SpaceNinjaServer#2557
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-07-29 00:31:19 -07:00
564aa06762 fix: correctly apply riven cipher (#2554)
The completeRandomModChallenge endpoint is only supposed to complete the challenge, what a shocker. Because we directly set a unveiled fingerprint, the game was not showing the expected UI.

Reviewed-on: OpenWF/SpaceNinjaServer#2554
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-27 06:29:12 -07:00
2e84f71af8 chore: faithful handling when ki'teer signa was rolled (#2553)
Reviewed-on: OpenWF/SpaceNinjaServer#2553
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-27 06:29:02 -07:00
ddfa98e0b2 chore: update baro.json (#2550)
Co-authored-by: BanLanGen <banlangen@noreply.localhost>
Reviewed-on: OpenWF/SpaceNinjaServer#2550
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-27 06:28:51 -07:00
bb3c3e01b0 chore: add GEAR loadout slot (#2545)
added missing GEAR loadout slot (was causing issues with saving loadout in U29)

Reviewed-on: OpenWF/SpaceNinjaServer#2545
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: azdful <mischzaripov@yandex.ru>
Co-committed-by: azdful <mischzaripov@yandex.ru>
2025-07-25 01:51:13 -07:00
695dcf98e0 chore: handle sale of fusion treasures (#2542)
Closes #2541

Reviewed-on: OpenWF/SpaceNinjaServer#2542
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-24 05:30:55 -07:00
509f7f0d9b feat: selling for Dirac (CrewShipFusionPoints) (#2540)
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Reviewed-on: OpenWF/SpaceNinjaServer#2540
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: azdful <mischzaripov@yandex.ru>
Co-committed-by: azdful <mischzaripov@yandex.ru>
2025-07-23 11:09:44 -07:00
aada031a80 chore: update mongoose (#2539)
The transform hook signature was changed in the typings, so I just updated them to be explicit about what we expect.

Reviewed-on: OpenWF/SpaceNinjaServer#2539
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-23 07:51:51 -07:00
a2a441ecb0 fix: getUsernameFromEmail returning wrong value (#2538)
Closes #2537

Reviewed-on: OpenWF/SpaceNinjaServer#2538
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-23 07:51:22 -07:00
c0a0463a68 feat: vista suite backdrop and soundscape customisation (#2534)
Closes #2532

Reviewed-on: OpenWF/SpaceNinjaServer#2534
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-22 07:34:46 -07:00
2307a40833 chore(webui): debounce inventory bulk actions (#2533)
Closes #2513

Reviewed-on: OpenWF/SpaceNinjaServer#2533
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-22 07:34:40 -07:00
304af514e2 fix(webui): handle name already being taken (#2530)
Closes #2528

Reviewed-on: OpenWF/SpaceNinjaServer#2530
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-22 07:34:31 -07:00
ddf3cd49b5 chore: handle new T value for orowyrm chest (#2527)
Closes #2526

Reviewed-on: OpenWF/SpaceNinjaServer#2527
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-22 07:34:22 -07:00
41e3f0136f chore(webui): improve auth state management 2025-07-21 20:28:19 +02:00
c0ca9d9398 fix: add try/catch around websocket message event handler (#2529)
Re #2528

Reviewed-on: OpenWF/SpaceNinjaServer#2529
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-21 07:44:54 -07:00
0f6b55beed chore(webui): update fr (#2525)
Reviewed-on: OpenWF/SpaceNinjaServer#2525
Co-authored-by: Vitruvio <vitruvio@noreply.localhost>
Co-committed-by: Vitruvio <vitruvio@noreply.localhost>
2025-07-21 03:23:12 -07:00
f8550e9afe fix: incorect deimos bounty medallion reward (#2524)
Reviewed-on: OpenWF/SpaceNinjaServer#2524
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-07-21 03:23:05 -07:00
b53c4d9125 feat: reset obstacle course (#2523)
Closes #2520

Reviewed-on: OpenWF/SpaceNinjaServer#2523
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-21 03:22:59 -07:00
922b65cfab chore: print build date when started via docker (#2517)
Docker updates can be a bit confusing so this should help users know if they're up-to-date.

Reviewed-on: OpenWF/SpaceNinjaServer#2517
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-21 03:22:46 -07:00
2f642df20a feat: reset decorations (#2516)
Closes #2514

Reviewed-on: OpenWF/SpaceNinjaServer#2516
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-21 03:22:35 -07:00
62314e89c7 fix: refund personal decos when destroying dojo room (#2522)
Closes #2521

Reviewed-on: OpenWF/SpaceNinjaServer#2522
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-20 10:35:14 -07:00
56aa3e3331 fix: placing decorations in apartment in newer game versions (#2515)
Newer game versions use BootLocation instead of IsApartment

Reviewed-on: OpenWF/SpaceNinjaServer#2515
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-20 01:29:08 -07:00
c3f486488f chore: npm update (#2512)
There were some low severity vulnerabilites audit was complaining about

Reviewed-on: OpenWF/SpaceNinjaServer#2512
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-20 00:28:46 -07:00
49c353d895 chore: disable DTLS (#2511)
Reviewed-on: OpenWF/SpaceNinjaServer#2511
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-20 00:28:37 -07:00
90ab560620 chore(webui): don't refresh inventory for sell on the tab that issued it (#2506)
Reviewed-on: OpenWF/SpaceNinjaServer#2506
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-18 15:36:10 -07:00
b0e80fcfa8 chore(webui): update German translation (#2507)
Reviewed-on: OpenWF/SpaceNinjaServer#2507
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-07-17 06:56:23 -07:00
2c62fb3c3c chore: move syncConfigWithDatabase to configService (#2505)
This avoids a cyclic dependency due to configController using this

Reviewed-on: OpenWF/SpaceNinjaServer#2505
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-17 05:04:30 -07:00
5b215733aa chore(webui): update to Spanish translation (#2503)
Reviewed-on: OpenWF/SpaceNinjaServer#2503
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-07-16 23:04:26 -07:00
39866b9a2b fix: hide edit suit invigorations card on detailed view load (#2500)
Fixes #2494

Co-authored-by: nyaoouo <64143453+nyaoouo@users.noreply.github.com>
Reviewed-on: OpenWF/SpaceNinjaServer#2500
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: nyaoouo <nyaoouo@noreply.localhost>
Co-committed-by: nyaoouo <nyaoouo@noreply.localhost>
2025-07-16 08:29:41 -07:00
fad1ee9314 chore(webui): update Chinese translation (#2501)
Reviewed-on: OpenWF/SpaceNinjaServer#2501
Co-authored-by: Corvus <corvus@noreply.localhost>
Co-committed-by: Corvus <corvus@noreply.localhost>
2025-07-16 08:29:13 -07:00
64b43fcccf chore(webui): fixing a mess in the translations (#2498)
Re #2494 (only fixes the strings, **NOT** the weapon issue)

- The invigoration stuff now mentions the numbers, percentages of buffs
- Improved some misleading strings (e.g. "Movement Speed", when it was in fact just "Sprint Speed" instead)
- Improved some inconsistencies in some key names (some weren't like other, similar existing ones)
- Got rid of duplicate "None" string & re-used it properly + re-used existing strings to newly added buttons, instead of using unnecessary extra added strings (more consistent to use existing strings, aside that they are shorter, less lines and less work overall for everyone involved)

If I should change anything, lemme know.

Reviewed-on: OpenWF/SpaceNinjaServer#2498
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-07-15 20:59:59 -07:00
e407262cf8 fix: don't send baro message ahead of his visit (#2497)
Closes #2495

Reviewed-on: OpenWF/SpaceNinjaServer#2497
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-15 20:59:49 -07:00
00e57c43df fix: charge correct amount of void traces for upgrading to radiant (#2492)
Closes #2490

Reviewed-on: OpenWF/SpaceNinjaServer#2492
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-15 20:59:39 -07:00
2ab9f39507 chore(webui): update Chinese translation (#2493)
Reviewed-on: OpenWF/SpaceNinjaServer#2493
Co-authored-by: Corvus <corvus@noreply.localhost>
Co-committed-by: Corvus <corvus@noreply.localhost>
2025-07-15 02:37:43 -07:00
b60723ef54 feat(webui): edit suit invigorations (#2478)
Co-authored-by: nyaoouo <64143453+nyaoouo@users.noreply.github.com>
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Reviewed-on: OpenWF/SpaceNinjaServer#2478
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: nyaoouo <nyaoouo@noreply.localhost>
Co-committed-by: nyaoouo <nyaoouo@noreply.localhost>
2025-07-14 20:33:37 -07:00
b3bf291d10 chore: send event messages for boosters (#2487)
Closes #2464

Reviewed-on: OpenWF/SpaceNinjaServer#2487
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-14 20:24:17 -07:00
db86e2d265 chore(webui): update translations (#2489)
- Updated German translation with all new strings
- Added the rest of the missing Chinese translation contributors (only the ones who did actually translate, that is)

Reviewed-on: OpenWF/SpaceNinjaServer#2489
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-07-14 20:23:56 -07:00
f6cb8414c1 chore(webui): refresh inventory when crafting/buying/gilding kitguns (#2486)
Reviewed-on: OpenWF/SpaceNinjaServer#2486
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-07-13 21:08:34 -07:00
ba3df4bdbc fix: don't give mastery xp for SpecialItems except for venari (#2484)
Closes #2482

Reviewed-on: OpenWF/SpaceNinjaServer#2484
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-13 21:08:23 -07:00
8feb3a5b3c feat: give kaithe summon at riding level 9 (#2483)
Closes #2480

Reviewed-on: OpenWF/SpaceNinjaServer#2483
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-13 21:08:14 -07:00
66f3d65d77 fix(webui): none syndicate (#2477)
Closes #2475

Reviewed-on: OpenWF/SpaceNinjaServer#2477
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-12 21:20:08 -07:00
b18f06087b chore(webui): update to Spanish translation (#2481)
Reviewed-on: OpenWF/SpaceNinjaServer#2481
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-07-12 21:19:50 -07:00
987b5b98ff chore(webui): update Chinese translation (#2476)
Reviewed-on: OpenWF/SpaceNinjaServer#2476
Co-authored-by: Corvus <corvus@noreply.localhost>
Co-committed-by: Corvus <corvus@noreply.localhost>
2025-07-12 00:00:23 -07:00
fbbd9076cf fix: delete galleon of ghouls inbox messages when disabled via webui (#2473)
Reviewed-on: OpenWF/SpaceNinjaServer#2473
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-11 21:16:07 -07:00
838818543c fix: omit plains of eidolon from non-grineer sorties (#2472)
Closes #2470

Reviewed-on: OpenWF/SpaceNinjaServer#2472
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-11 21:15:48 -07:00
a16e2716f1 feat(webui): Change weapon Modular Parts (#2471)
Reviewed-on: OpenWF/SpaceNinjaServer#2471
Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-07-11 21:15:16 -07:00
f4c7ce582b chore(webui): handle malformed rivens so they can be deleted at least (#2469)
Closes #2468

Reviewed-on: OpenWF/SpaceNinjaServer#2469
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-11 21:15:04 -07:00
c0187f9446 chore(webui): refresh inventory when pet was consigned (#2467)
Closes #2463

Reviewed-on: OpenWF/SpaceNinjaServer#2467
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-11 08:55:04 -07:00
f796f9a851 feat: resetQuestProgress (#2461)
Just giving the client an 'ok' response. It seems that it does use updateQuest to manage the state itself mostly, just the server and webui are a bit confused about a quest with all stages completed still being active.
Re #1323

Reviewed-on: OpenWF/SpaceNinjaServer#2461
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-10 20:59:39 -07:00
e18b8e09ea fix: properly track xp for modular items (#2460)
Closes #2454

Reviewed-on: OpenWF/SpaceNinjaServer#2460
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-10 20:59:32 -07:00
0d8044b87c chore(webui): update to Spanish translation (#2466)
Reviewed-on: OpenWF/SpaceNinjaServer#2466
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-07-10 20:59:22 -07:00
a109ea6c5d chore: update PE+ (#2459)
Closes #2455

Reviewed-on: OpenWF/SpaceNinjaServer#2459
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-09 21:22:23 -07:00
7eb95c995c feat: initial invasions (#2458)
A rough generation of 3 invasions that change at daily reset, so missing the planet-based invasion 'chains'.
Battle pay is fully working tho, just a few points of uncertainty there due to missing research and logs.
Death marks are also roughly working.
Re #1097

Reviewed-on: OpenWF/SpaceNinjaServer#2458
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-09 19:58:01 -07:00
dc8f32d4d8 chore(webui): update Chinese translation (#2453)
Reviewed-on: OpenWF/SpaceNinjaServer#2453
Co-authored-by: Corvus <corvus@noreply.localhost>
Co-committed-by: Corvus <corvus@noreply.localhost>
2025-07-08 22:12:26 -07:00
ba70ba88dd fix(webui): recreate missing datalist-QuestKeys entries after refreshing inventory (#2452)
Closes #2448

Reviewed-on: OpenWF/SpaceNinjaServer#2452
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-08 20:52:45 -07:00
08d4a03c50 fix: use the correct magic number for crew member seeds (#2451)
Closes #2444

Reviewed-on: OpenWF/SpaceNinjaServer#2451
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-08 20:52:24 -07:00
45feff682b feat: give on call crew gear item for command rank 9 (#2450)
Closes #2445

Reviewed-on: OpenWF/SpaceNinjaServer#2450
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-08 20:52:14 -07:00
65be1083ce feat(webui): mark inbox as read (#2449)
Closes #1117

Reviewed-on: OpenWF/SpaceNinjaServer#2449
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-08 20:52:01 -07:00
07e7c9e897 fix: add eudico's post-new war yapping to allDialogue (#2447)
Reviewed-on: OpenWF/SpaceNinjaServer#2447
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-08 20:51:46 -07:00
dcb26471c9 feat: handle all slot types in inventorySlots.php (#2443)
Reviewed-on: OpenWF/SpaceNinjaServer#2443
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-08 20:51:35 -07:00
5a75d88385 feat: give skiajati and umbra mods alongside umbra, with max rank and potatoes (#2442)
Not 100% sure if the response format is correct and if this is even the correct time/place to do it, but impossible to say without a log from live.

Closes #1054

Reviewed-on: OpenWF/SpaceNinjaServer#2442
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-08 20:51:18 -07:00
a35572e306 fix(webui): move "add maxed" after "add" button (#2441)
Apparently the onclick event is being fired even when pressing enter. I originally moved it to make alt+enter add it maxed, but now even just pressing enter adds it maxed which is not what I wanted. :|

Reviewed-on: OpenWF/SpaceNinjaServer#2441
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-08 20:50:51 -07:00
c46c43f143 chore(webui): add loading string to translation system (#2440)
Closes #2439

Reviewed-on: OpenWF/SpaceNinjaServer#2440
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-08 20:50:28 -07:00
98ed2b5ee4 chore: use ideal time when going backwards to satisfy constraints (#2438)
"Before next expected world state refresh" is now used as a bare minimum constraint. If it cannot be met, we align to the ideal second. Compromising when multiple constraints are in use to avoid having to go back like 7 years, as this would break navigation.

Closes #2434

Reviewed-on: OpenWF/SpaceNinjaServer#2438
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-08 20:50:10 -07:00
7aa1b12306 fix: show multiplied relic reward amount on eom screen (#2437)
Reviewed-on: OpenWF/SpaceNinjaServer#2437
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-08 20:49:46 -07:00
b410f6b554 chore(webui): indicate unsaved changes (#2436)
Reviewed-on: OpenWF/SpaceNinjaServer#2436
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-08 20:49:22 -07:00
1dffcf979f feat: send tennokai email after WitW quest completion (#2433)
Reviewed-on: OpenWF/SpaceNinjaServer#2433
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-08 00:29:16 -07:00
c86bba017b chore(webui): update Chinese translation (#2432)
Reviewed-on: OpenWF/SpaceNinjaServer#2432
Co-authored-by: Corvus <corvus@noreply.localhost>
Co-committed-by: Corvus <corvus@noreply.localhost>
2025-07-08 00:29:05 -07:00
2c499cec3d fix: set proper dominant traits for helminth charger (#2429)
Closes #2417

Reviewed-on: OpenWF/SpaceNinjaServer#2429
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-08 00:28:56 -07:00
d6145561fd chore: improve randomness of void storm missions (#2428)
Instead of alternating the mission pool every hour, we now use sequentiallyUniqueRandomElement which should ensure that we don't duplicate any of the last x missions (x = 3 for Lith & Axi and x = 1 Meso & Neo).

Reviewed-on: OpenWF/SpaceNinjaServer#2428
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-08 00:28:49 -07:00
1545cdb8ce fix(webui): handle config having no worldState entry at all (#2427)
Reviewed-on: OpenWF/SpaceNinjaServer#2427
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-06 20:14:05 -07:00
80b5e2df7f feat: random recessive traits for beasts (#2426)
Reviewed-on: OpenWF/SpaceNinjaServer#2426
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-06 20:13:59 -07:00
76e61129bf fix: skip birthdays of characters we can't talk to (#2425)
Closes #2424

Reviewed-on: OpenWF/SpaceNinjaServer#2425
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-06 20:13:50 -07:00
ea3e299861 fix: ensure nightwave weekly challenges are unique (#2423)
Re #2411

Reviewed-on: OpenWF/SpaceNinjaServer#2423
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-06 20:13:42 -07:00
3d8c1d036a fix: ensure nightwave daily challenges are unique (#2422)
When generating a daily challenge, we now use sequentiallyUniqueRandomElement with a lookbehind of 2 to ensure the 2 previous (and still active) daily challenges are not duplicated.

Re #2411

Reviewed-on: OpenWF/SpaceNinjaServer#2422
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-06 20:13:32 -07:00
773f96ebbc fix: set PrimeTokenAvailability to true (#2420)
Closes #2416

Reviewed-on: OpenWF/SpaceNinjaServer#2420
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-06 20:13:20 -07:00
2a80307c26 chore(webui): reuse "code_remove" for "general_removeButton" (#2421)
Closes #2418

Reviewed-on: OpenWF/SpaceNinjaServer#2421
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-05 20:18:18 -07:00
a40ff27fea fix: add InitialStartDate to PrimeVaultTrader (#2419)
Closes #2414

Reviewed-on: OpenWF/SpaceNinjaServer#2419
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-05 20:18:01 -07:00
c9a4359714 chore(webui): update Chinese translation (#2413)
Reviewed-on: OpenWF/SpaceNinjaServer#2413
Co-authored-by: Corvus <corvus@noreply.localhost>
Co-committed-by: Corvus <corvus@noreply.localhost>
2025-07-05 16:52:16 -07:00
280ed8bef1 chore(webui): improve string (#2412)
The `100% chance` part is unnecessary in practice and can be shortened. In-game it also never mentions the `100% chance` part either.

Reviewed-on: OpenWF/SpaceNinjaServer#2412
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-07-05 06:41:26 -07:00
9c89e907b1 chore(webui): update Chinese translation (#2410)
Reviewed-on: OpenWF/SpaceNinjaServer#2410
Co-authored-by: Corvus <corvus@noreply.localhost>
Co-committed-by: Corvus <corvus@noreply.localhost>
2025-07-05 06:23:17 -07:00
b54fd96098 chore: fix cyclic includes due to saveConfig used in controllers (#2409)
Reviewed-on: OpenWF/SpaceNinjaServer#2409
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-07-05 06:23:10 -07:00
107 changed files with 6946 additions and 1995 deletions

View File

@ -21,7 +21,7 @@
"@typescript-eslint/no-unsafe-argument": "error",
"@typescript-eslint/no-unsafe-call": "error",
"@typescript-eslint/no-unsafe-assignment": "error",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-explicit-any": "off",
"no-loss-of-precision": "error",
"@typescript-eslint/no-unnecessary-condition": "error",
"@typescript-eslint/no-base-to-string": "off",

View File

@ -14,7 +14,7 @@ jobs:
with:
node-version: ">=20.6.0"
- run: npm ci
- run: cp config.json.example config.json
- run: cp config-vanilla.json config.json
- run: npm run verify
- run: npm run lint:ci
- run: npm run prettier

View File

@ -2,3 +2,4 @@ src/routes/api.ts
static/webui/libs/
*.html
*.md
config-vanilla.json

View File

@ -7,5 +7,6 @@ WORKDIR /app
RUN npm i --omit=dev
RUN npm run build
RUN date '+%d %B %Y' > BUILD_DATE
ENTRYPOINT ["/app/docker-entrypoint.sh"]

View File

@ -10,7 +10,7 @@ To get an idea of what functionality you can expect to be missing [have a look t
## config.json
SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [config.json.example](config.json.example), which has most cheats disabled.
SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [config-vanilla.json](config-vanilla.json), which has most cheats disabled.
- `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`.
- `myIrcAddresses` can be used to point to an IRC server. If not provided, defaults to `[ myAddress ]`.

View File

@ -13,11 +13,6 @@
"skipTutorial": false,
"skipAllDialogue": false,
"unlockAllScans": false,
"infiniteCredits": false,
"infinitePlatinum": false,
"infiniteEndo": false,
"infiniteRegalAya": false,
"infiniteHelminthMaterials": false,
"claimingBlueprintRefundsIngredients": false,
"dontSubtractPurchaseCreditCost": false,
"dontSubtractPurchasePlatinumCost": false,
@ -70,8 +65,24 @@
"creditBoost": false,
"affinityBoost": false,
"resourceBoost": false,
"starDays": true,
"tennoLiveRelay": false,
"wolfHunt": false,
"longShadow": false,
"hallowedFlame": false,
"hallowedNightmares": false,
"hallowedNightmaresRewardsOverride": 0,
"proxyRebellion": false,
"proxyRebellionRewardsOverride": 0,
"galleonOfGhouls": 0,
"ghoulEmergenceOverride": null,
"plagueStarOverride": null,
"starDaysOverride": null,
"dogDaysOverride": null,
"dogDaysRewardsOverride": null,
"bellyOfTheBeast": false,
"bellyOfTheBeastProgressOverride": 0,
"eightClaw": false,
"eightClawProgressOverride": 0,
"eidolonOverride": "",
"vallisOverride": "",
"duviriOverride": "",

View File

@ -2,7 +2,7 @@
set -e
if [ ! -f conf/config.json ]; then
jq --arg value "mongodb://openwfagent:spaceninjaserver@mongodb:27017/" '.mongodbUrl = $value' /app/config.json.example > /app/conf/config.json
jq --arg value "mongodb://openwfagent:spaceninjaserver@mongodb:27017/" '.mongodbUrl = $value' /app/config-vanilla.json > /app/conf/config.json
fi
exec npm run start -- --configPath conf/config.json

660
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,7 @@
"dev": "node scripts/dev.js",
"dev:bun": "bun scripts/dev.js",
"verify": "tsgo --noEmit",
"verify:tsc": "tsc --noEmit",
"bun-run": "bun src/index.ts",
"lint": "eslint --ext .ts .",
"lint:ci": "eslint --ext .ts --rule \"prettier/prettier: off\" .",
@ -39,7 +40,7 @@
"ncp": "^2.0.0",
"typescript": "^5.5",
"undici": "^7.10.0",
"warframe-public-export-plus": "^0.5.77",
"warframe-public-export-plus": "^0.5.81",
"warframe-riven-info": "^0.1.2",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0",

View File

@ -0,0 +1,22 @@
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getPersonalRooms } from "@/src/services/personalRoomsService";
import { RequestHandler } from "express";
export const apartmentController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const personalRooms = await getPersonalRooms(accountId, "Apartment");
const response: IApartmentResponse = {};
if (req.query.backdrop !== undefined) {
response.NewBackdropItem = personalRooms.Apartment.VideoWallBackdrop = req.query.backdrop as string;
}
if (req.query.soundscape !== undefined) {
response.NewSoundscapeItem = personalRooms.Apartment.Soundscape = req.query.soundscape as string;
}
await personalRooms.save();
res.json(response);
};
interface IApartmentResponse {
NewBackdropItem?: string;
NewSoundscapeItem?: string;
}

View File

@ -3,7 +3,6 @@ import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
import { IInventoryClient, IUpgradeClient } from "@/src/types/inventoryTypes/inventoryTypes";
import { addMods, getInventory } from "@/src/services/inventoryService";
import { config } from "@/src/services/configService";
export const artifactsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -34,10 +33,10 @@ export const artifactsController: RequestHandler = async (req, res) => {
addMods(inventory, [{ ItemType, ItemCount: -1 }]);
}
if (!config.infiniteCredits) {
if (!inventory.infiniteCredits) {
inventory.RegularCredits -= Cost;
}
if (!config.infiniteEndo) {
if (!inventory.infiniteEndo) {
inventory.FusionPoints -= FusionPointCost;
}

View File

@ -14,7 +14,9 @@ import {
addRecipes,
occupySlot,
combineInventoryChanges,
addKubrowPetPrint
addKubrowPetPrint,
addPowerSuit,
addEquipment
} from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { InventorySlot, IPendingRecipeDatabase } from "@/src/types/inventoryTypes/inventoryTypes";
@ -22,7 +24,7 @@ import { toOid2 } from "@/src/helpers/inventoryHelpers";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { IRecipe } from "warframe-public-export-plus";
import { config } from "@/src/services/configService";
import { IEquipmentClient, Status } from "@/src/types/equipmentTypes";
import { EquipmentFeatures, IEquipmentClient, Status } from "@/src/types/equipmentTypes";
interface IClaimCompletedRecipeRequest {
RecipeIds: IOid[];
@ -100,7 +102,10 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
const secondsElapsed = Math.trunc(Date.now() / 1000) - start;
const progress = secondsElapsed / recipe.buildTime;
logger.debug(`rushing recipe at ${Math.trunc(progress * 100)}% completion`);
const cost = Math.round(recipe.skipBuildTimePrice * (1 - (progress - 0.5)));
const cost =
progress > 0.5
? Math.round(recipe.skipBuildTimePrice * (1 - (progress - 0.5)))
: recipe.skipBuildTimePrice;
InventoryChanges = {
...InventoryChanges,
...updateCurrency(inventory, cost, true)
@ -124,17 +129,122 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
const pet = inventory.KubrowPets.id(pendingRecipe.KubrowPet!)!;
addKubrowPetPrint(inventory, pet, InventoryChanges);
} else if (recipe.secretIngredientAction != "SIA_UNBRAND") {
InventoryChanges = {
...InventoryChanges,
...(await addItem(
if (recipe.resultType == "/Lotus/Powersuits/Excalibur/ExcaliburUmbra") {
// Quite the special case here...
// We don't just get Umbra, but also Skiajati and Umbra Mods. Both items are max rank, potatoed, and with the mods are pre-installed.
// Source: https://wiki.warframe.com/w/The_Sacrifice, https://wiki.warframe.com/w/Excalibur/Umbra, https://wiki.warframe.com/w/Skiajati
const umbraModA = (
await addItem(
inventory,
"/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModA",
1,
false,
undefined,
`{"lvl":5}`
)
).Upgrades![0];
const umbraModB = (
await addItem(
inventory,
"/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModB",
1,
false,
undefined,
`{"lvl":5}`
)
).Upgrades![0];
const umbraModC = (
await addItem(
inventory,
"/Lotus/Upgrades/Mods/Sets/Umbra/WarframeUmbraModC",
1,
false,
undefined,
`{"lvl":5}`
)
).Upgrades![0];
const sacrificeModA = (
await addItem(
inventory,
"/Lotus/Upgrades/Mods/Sets/Sacrifice/MeleeSacrificeModA",
1,
false,
undefined,
`{"lvl":5}`
)
).Upgrades![0];
const sacrificeModB = (
await addItem(
inventory,
"/Lotus/Upgrades/Mods/Sets/Sacrifice/MeleeSacrificeModB",
1,
false,
undefined,
`{"lvl":5}`
)
).Upgrades![0];
InventoryChanges.Upgrades ??= [];
InventoryChanges.Upgrades.push(umbraModA, umbraModB, umbraModC, sacrificeModA, sacrificeModB);
await addPowerSuit(
inventory,
recipe.resultType,
recipe.num,
false,
undefined,
pendingRecipe.TargetFingerprint
))
};
"/Lotus/Powersuits/Excalibur/ExcaliburUmbra",
{
Configs: [
{
Upgrades: [
"",
"",
"",
"",
"",
umbraModA.ItemId.$oid,
umbraModB.ItemId.$oid,
umbraModC.ItemId.$oid
]
}
],
XP: 900_000,
Features: EquipmentFeatures.DOUBLE_CAPACITY
},
InventoryChanges
);
inventory.XPInfo.push({
ItemType: "/Lotus/Powersuits/Excalibur/ExcaliburUmbra",
XP: 900_000
});
addEquipment(
inventory,
"Melee",
"/Lotus/Weapons/Tenno/Melee/Swords/UmbraKatana/UmbraKatana",
{
Configs: [
{ Upgrades: ["", "", "", "", "", "", sacrificeModA.ItemId.$oid, sacrificeModB.ItemId.$oid] }
],
XP: 450_000,
Features: EquipmentFeatures.DOUBLE_CAPACITY
},
InventoryChanges
);
inventory.XPInfo.push({
ItemType: "/Lotus/Weapons/Tenno/Melee/Swords/UmbraKatana/UmbraKatana",
XP: 450_000
});
} else {
InventoryChanges = {
...InventoryChanges,
...(await addItem(
inventory,
recipe.resultType,
recipe.num,
false,
undefined,
pendingRecipe.TargetFingerprint
))
};
}
}
if (
config.claimingBlueprintRefundsIngredients &&

View File

@ -1,4 +1,4 @@
import { checkCalendarChallengeCompletion, getCalendarProgress, getInventory } from "@/src/services/inventoryService";
import { checkCalendarAutoAdvance, getCalendarProgress, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { getWorldState } from "@/src/services/worldStateService";
@ -28,7 +28,7 @@ export const completeCalendarEventController: RequestHandler = async (req, res)
}
}
calendarProgress.SeasonProgress.LastCompletedDayIdx = dayIndex;
checkCalendarChallengeCompletion(calendarProgress, currentSeason);
checkCalendarAutoAdvance(inventory, currentSeason);
await inventory.save();
res.json({
InventoryChanges: inventoryChanges,

View File

@ -4,8 +4,7 @@ import { addMiscItems, getInventory, updateCurrency } from "@/src/services/inven
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { createUnveiledRivenFingerprint } from "@/src/helpers/rivenHelper";
import { ExportUpgrades } from "warframe-public-export-plus";
import { IVeiledRivenFingerprint } from "@/src/helpers/rivenHelper";
export const completeRandomModChallengeController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -27,10 +26,11 @@ export const completeRandomModChallengeController: RequestHandler = async (req,
inventoryChanges.MiscItems = miscItemChanges;
}
// Update riven fingerprint to a randomised unveiled state
// Complete the riven challenge
const upgrade = inventory.Upgrades.id(request.ItemId)!;
const meta = ExportUpgrades[upgrade.ItemType];
upgrade.UpgradeFingerprint = JSON.stringify(createUnveiledRivenFingerprint(meta));
const fp = JSON.parse(upgrade.UpgradeFingerprint!) as IVeiledRivenFingerprint;
fp.challenge.Progress = fp.challenge.Required;
upgrade.UpgradeFingerprint = JSON.stringify(fp);
await inventory.save();

View File

@ -1,5 +1,4 @@
import { RequestHandler } from "express";
import { config } from "@/src/services/configService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService";
@ -9,7 +8,7 @@ export const creditsController: RequestHandler = async (req, res) => {
getAccountIdForRequest(req),
getInventory(
req.query.accountId as string,
"RegularCredits TradesRemaining PremiumCreditsFree PremiumCredits"
"RegularCredits TradesRemaining PremiumCreditsFree PremiumCredits infiniteCredits infinitePlatinum"
)
])
)[1];
@ -21,10 +20,10 @@ export const creditsController: RequestHandler = async (req, res) => {
PremiumCredits: inventory.PremiumCredits
};
if (config.infiniteCredits) {
if (inventory.infiniteCredits) {
response.RegularCredits = 999999999;
}
if (config.infinitePlatinum) {
if (inventory.infinitePlatinum) {
response.PremiumCreditsFree = 0;
response.PremiumCredits = 999999999;
}

View File

@ -88,7 +88,6 @@ export const crewShipFusionController: RequestHandler = async (req, res) => {
}
}
superiorItem.UpgradeFingerprint = JSON.stringify(fingerprint);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
inventoryChanges[category] = [superiorItem.toJSON() as any];
await inventory.save();

View File

@ -3,11 +3,13 @@ import {
getGuildForRequestEx,
hasAccessToDojo,
hasGuildPermission,
refundDojoDeco,
removeDojoDeco
} from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express";
export const destroyDojoDecoController: RequestHandler = async (req, res) => {
@ -18,9 +20,20 @@ export const destroyDojoDecoController: RequestHandler = async (req, res) => {
res.json({ DojoRequestStatus: -1 });
return;
}
const request = JSON.parse(String(req.body)) as IDestroyDojoDecoRequest;
removeDojoDeco(guild, request.ComponentId, request.DecoId);
const request = JSON.parse(String(req.body)) as IDestroyDojoDecoRequest | IClearObstacleCourseRequest;
if ("DecoType" in request) {
removeDojoDeco(guild, request.ComponentId, request.DecoId);
} else if (request.Act == "cObst") {
const component = guild.DojoComponents.id(request.ComponentId)!;
if (component.Decos) {
for (const deco of component.Decos) {
refundDojoDeco(guild, component, deco);
}
component.Decos.splice(0, component.Decos.length);
}
} else {
logger.error(`unhandled destroyDojoDeco request`, request);
}
await guild.save();
res.json(await getDojoClient(guild, 0, request.ComponentId));
@ -31,3 +44,8 @@ interface IDestroyDojoDecoRequest {
ComponentId: string;
DecoId: string;
}
interface IClearObstacleCourseRequest {
ComponentId: string;
Act: "cObst" | "maybesomethingelsewedontknowabout";
}

View File

@ -72,7 +72,7 @@ export const dronesController: RequestHandler = async (req, res) => {
);
}
} else {
drone.ResourceCount = 1;
drone.ResourceCount = droneMeta.binCapacity * droneMeta.capacityMultipliers[resource.Rarity];
}
await inventory.save();
res.json({});

View File

@ -2,22 +2,14 @@ import { RequestHandler } from "express";
import { ExportResources } from "warframe-public-export-plus";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { addFusionTreasures, addMiscItems, getInventory } from "@/src/services/inventoryService";
import { IFusionTreasure, IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { parseFusionTreasure } from "@/src/helpers/inventoryHelpers";
interface IFusionTreasureRequest {
oldTreasureName: string;
newTreasureName: string;
}
const parseFusionTreasure = (name: string, count: number): IFusionTreasure => {
const arr = name.split("_");
return {
ItemType: arr[0],
Sockets: parseInt(arr[1], 16),
ItemCount: count
};
};
export const fusionTreasuresController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);

View File

@ -0,0 +1,25 @@
import { RequestHandler } from "express";
import { getAccountForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService";
import { Guild } from "@/src/models/guildModel";
export const getGuildEventScoreController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req);
const inventory = await getInventory(account._id.toString(), "GuildId");
const guild = await Guild.findById(inventory.GuildId);
const goalId = req.query.goalId as string;
if (guild && guild.GoalProgress && goalId) {
const goal = guild.GoalProgress.find(x => x.goalId.toString() == goalId);
if (goal) {
return res.json({
Tier: guild.Tier,
GoalProgress: {
Count: goal.Count,
Tag: goal.Tag,
_id: { $oid: goal.goalId }
}
});
}
}
return res.json({});
};

View File

@ -0,0 +1,62 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService";
import { EPOCH, getSeasonChallengePools, getWorldState, pushWeeklyActs } from "@/src/services/worldStateService";
import { unixTimesInMs } from "@/src/constants/timeConstants";
import { ISeasonChallenge } from "@/src/types/worldStateTypes";
import { ExportChallenges } from "warframe-public-export-plus";
export const getPastWeeklyChallengesController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "SeasonChallengeHistory ChallengeProgress");
const worldState = getWorldState(undefined);
if (worldState.SeasonInfo) {
const pools = getSeasonChallengePools(worldState.SeasonInfo.AffiliationTag);
const nightwaveStartTimestamp = Number(worldState.SeasonInfo.Activation.$date.$numberLong);
const nightwaveSeason = worldState.SeasonInfo.Season;
const timeMs = worldState.Time * 1000;
const completedChallengesIds = new Set<string>();
inventory.SeasonChallengeHistory.forEach(challengeHistory => {
const entryNightwaveSeason = parseInt(challengeHistory.id.slice(0, 4), 10) - 1;
if (nightwaveSeason == entryNightwaveSeason) {
const meta = Object.entries(ExportChallenges).find(
([key]) => key.split("/").pop() === challengeHistory.challenge
);
if (meta) {
const [, challengeMeta] = meta;
const challengeProgress = inventory.ChallengeProgress.find(
c => c.Name === challengeHistory.challenge
);
if (challengeProgress && challengeProgress.Progress >= (challengeMeta.requiredCount ?? 1)) {
completedChallengesIds.add(challengeHistory.id);
}
}
}
});
const PastWeeklyChallenges: ISeasonChallenge[] = [];
let week = Math.trunc((timeMs - EPOCH) / unixTimesInMs.week) - 1;
while (EPOCH + week * unixTimesInMs.week >= nightwaveStartTimestamp && PastWeeklyChallenges.length < 3) {
const tempActs: ISeasonChallenge[] = [];
pushWeeklyActs(tempActs, pools, week, nightwaveStartTimestamp, nightwaveSeason);
for (const act of tempActs) {
if (!completedChallengesIds.has(act._id.$oid) && PastWeeklyChallenges.length < 3) {
if (act.Challenge.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent")) {
act.Permanent = true;
}
PastWeeklyChallenges.push(act);
}
}
week--;
}
res.json({ PastWeeklyChallenges: PastWeeklyChallenges });
}
};

View File

@ -1,5 +1,6 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { sendWsBroadcastTo } from "@/src/services/wsService";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addMiscItems, getInventory } from "@/src/services/inventoryService";
import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
@ -73,4 +74,5 @@ export const gildWeaponController: RequestHandler = async (req, res) => {
InventoryChanges: inventoryChanges,
AffiliationMods: affiliationMods
});
sendWsBroadcastTo(accountId, { update_inventory: true });
};

View File

@ -12,7 +12,7 @@ export const hubBlessingController: RequestHandler = async (req, res) => {
if (req.query.mode == "send") {
const inventory = await getInventory(accountId, "BlessingCooldown Boosters");
inventory.BlessingCooldown = new Date(Date.now() + 86400000);
addBooster(boosterType, 3 * 3600, inventory);
addBooster(boosterType, 3 * 3600, inventory); // unfaithful, but current HUB server does not handle broadcasting, so this way users can bless themselves.
await inventory.save();
let token = "";

View File

@ -13,7 +13,8 @@ import {
addItems,
combineInventoryChanges,
getEffectiveAvatarImageType,
getInventory
getInventory,
updateCurrency
} from "@/src/services/inventoryService";
import { logger } from "@/src/utils/logger";
import { ExportFlavour } from "warframe-public-export-plus";
@ -100,6 +101,9 @@ export const inboxController: RequestHandler = async (req, res) => {
}
}
}
if (message.RegularCredits) {
updateCurrency(inventory, -message.RegularCredits, false, inventoryChanges);
}
await inventory.save();
res.json({ InventoryChanges: inventoryChanges });
} else if (latestClientMessageId) {

View File

@ -15,7 +15,6 @@ import { getRecipe } from "@/src/services/itemDataService";
import { toMongoDate, version_compare } from "@/src/helpers/inventoryHelpers";
import { logger } from "@/src/utils/logger";
import { colorToShard } from "@/src/helpers/shardHelper";
import { config } from "@/src/services/configService";
import {
addInfestedFoundryXP,
applyCheatsToInfestedFoundry,
@ -73,7 +72,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
addMiscItems(inventory, miscItemChanges);
// consume resources
if (!config.infiniteHelminthMaterials) {
if (!inventory.infiniteHelminthMaterials) {
let type: string;
let count: number;
if (account.BuildLabel && version_compare(account.BuildLabel, "2025.05.20.10.18") < 0) {
@ -99,7 +98,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
await inventory.save();
const infestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry!;
applyCheatsToInfestedFoundry(infestedFoundry);
applyCheatsToInfestedFoundry(inventory, infestedFoundry);
res.json({
InventoryChanges: {
MiscItems: miscItemChanges,
@ -129,13 +128,14 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
case "c": {
// consume items
if (config.infiniteHelminthMaterials) {
const inventory = await getInventory(account._id.toString());
if (inventory.infiniteHelminthMaterials) {
res.status(400).end();
return;
}
const request = getJSONfromString<IHelminthFeedRequest>(String(req.body));
const inventory = await getInventory(account._id.toString());
inventory.InfestedFoundry ??= {};
inventory.InfestedFoundry.Resources ??= [];
@ -240,7 +240,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
}
await inventory.save();
const infestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry!;
applyCheatsToInfestedFoundry(infestedFoundry);
applyCheatsToInfestedFoundry(inventory, infestedFoundry);
res.json({
InventoryChanges: {
InfestedFoundry: infestedFoundry
@ -254,7 +254,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
const request = getJSONfromString<IHelminthSubsumeRequest>(String(req.body));
const inventory = await getInventory(account._id.toString());
const recipe = getRecipe(request.Recipe)!;
if (!config.infiniteHelminthMaterials) {
if (!inventory.infiniteHelminthMaterials) {
for (const ingredient of recipe.secretIngredients!) {
const resource = inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == ingredient.ItemType);
if (resource) {
@ -280,7 +280,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
freeUpSlot(inventory, InventorySlot.SUITS);
await inventory.save();
const infestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry!;
applyCheatsToInfestedFoundry(infestedFoundry);
applyCheatsToInfestedFoundry(inventory, infestedFoundry);
res.json({
InventoryChanges: {
Recipes: recipeChanges,
@ -307,7 +307,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
const recipeChanges = handleSubsumeCompletion(inventory);
await inventory.save();
const infestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry!;
applyCheatsToInfestedFoundry(infestedFoundry);
applyCheatsToInfestedFoundry(inventory, infestedFoundry);
res.json({
InventoryChanges: {
...currencyChanges,
@ -328,7 +328,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
suit.UpgradesExpiry = upgradesExpiry;
const recipeChanges = addInfestedFoundryXP(inventory.InfestedFoundry!, 4800_00);
addRecipes(inventory, recipeChanges);
if (!config.infiniteHelminthMaterials) {
if (!inventory.infiniteHelminthMaterials) {
for (let i = 0; i != request.ResourceTypes.length; ++i) {
inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == request.ResourceTypes[i])!.Count -=
request.ResourceCosts[i];
@ -338,7 +338,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
inventory.InfestedFoundry!.InvigorationsApplied += 1;
await inventory.save();
const infestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry!;
applyCheatsToInfestedFoundry(infestedFoundry);
applyCheatsToInfestedFoundry(inventory, infestedFoundry);
res.json({
SuitId: request.SuitId,
OffensiveUpgrade: request.OffensiveUpgradeType,

View File

@ -6,12 +6,21 @@ import allDialogue from "@/static/fixed_responses/allDialogue.json";
import { ILoadoutDatabase } from "@/src/types/saveLoadoutTypes";
import { IInventoryClient, IShipInventory, equipmentKeys } from "@/src/types/inventoryTypes/inventoryTypes";
import { IPolarity, ArtifactPolarity } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { ExportCustoms, ExportFlavour, ExportResources, ExportVirtuals } from "warframe-public-export-plus";
import {
eFaction,
ExportCustoms,
ExportFlavour,
ExportResources,
ExportVirtuals,
ICountedItem
} from "warframe-public-export-plus";
import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "@/src/services/infestedFoundryService";
import {
addEmailItem,
addItem,
addMiscItems,
allDailyAffiliationKeys,
checkCalendarAutoAdvance,
cleanupInventory,
createLibraryDailyTask,
getCalendarProgress
@ -29,6 +38,8 @@ import { unixTimesInMs } from "@/src/constants/timeConstants";
import { DailyDeal } from "@/src/models/worldStateModel";
import { EquipmentFeatures } from "@/src/types/equipmentTypes";
import { generateRewardSeed } from "@/src/services/rngService";
import { getInvasionByOid, getWorldState } from "@/src/services/worldStateService";
import { createMessage } from "@/src/services/inboxService";
export const inventoryController: RequestHandler = async (request, response) => {
const account = await getAccountForRequest(request);
@ -111,58 +122,61 @@ export const inventoryController: RequestHandler = async (request, response) =>
}
}
if (inventory.CalendarProgress) {
const previousYearIteration = inventory.CalendarProgress.Iteration;
getCalendarProgress(inventory); // handle year rollover; the client expects to receive an inventory with an up-to-date CalendarProgress
// TODO: Setup CalendarProgress as part of 1999 mission completion?
// also handle sending of kiss cinematic at year rollover
if (
inventory.CalendarProgress.Iteration != previousYearIteration &&
inventory.DialogueHistory &&
inventory.DialogueHistory.Dialogues
) {
let kalymos = false;
for (const { dialogueName, kissEmail } of [
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/ArthurDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/ArthurKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/EleanorDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/EleanorKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/LettieDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/LettieKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/JabirDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/AmirKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/AoiDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/AoiKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/QuincyDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/QuincyKissEmailItem"
const previousYearIteration = inventory.CalendarProgress?.Iteration;
// We need to do the following to ensure the in-game calendar does not break:
getCalendarProgress(inventory); // Keep the CalendarProgress up-to-date (at least for the current year iteration) (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/2364)
checkCalendarAutoAdvance(inventory, getWorldState().KnownCalendarSeasons[0]); // Skip birthday events for characters if we do not have them unlocked yet (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/2424)
// also handle sending of kiss cinematic at year rollover
if (
inventory.CalendarProgress!.Iteration != previousYearIteration &&
inventory.DialogueHistory &&
inventory.DialogueHistory.Dialogues
) {
let kalymos = false;
for (const { dialogueName, kissEmail } of [
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/ArthurDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/ArthurKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/EleanorDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/EleanorKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/LettieDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/LettieKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/JabirDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/AmirKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/AoiDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/AoiKissEmailItem"
},
{
dialogueName: "/Lotus/Types/Gameplay/1999Wf/Dialogue/QuincyDialogue_rom.dialogue",
kissEmail: "/Lotus/Types/Items/EmailItems/QuincyKissEmailItem"
}
]) {
const dialogue = inventory.DialogueHistory.Dialogues.find(x => x.DialogueName == dialogueName);
if (dialogue) {
if (dialogue.Rank == 7) {
await addEmailItem(inventory, kissEmail);
kalymos = false;
break;
}
]) {
const dialogue = inventory.DialogueHistory.Dialogues.find(x => x.DialogueName == dialogueName);
if (dialogue) {
if (dialogue.Rank == 7) {
await addEmailItem(inventory, kissEmail);
kalymos = false;
break;
}
if (dialogue.Rank == 6) {
kalymos = true;
}
if (dialogue.Rank == 6) {
kalymos = true;
}
}
if (kalymos) {
await addEmailItem(inventory, "/Lotus/Types/Items/EmailItems/KalymosKissEmailItem");
}
}
if (kalymos) {
await addEmailItem(inventory, "/Lotus/Types/Items/EmailItems/KalymosKissEmailItem");
}
}
@ -181,6 +195,63 @@ export const inventoryController: RequestHandler = async (request, response) =>
//await inventory.save();
}
for (let i = 0; i != inventory.QualifyingInvasions.length; ) {
const qi = inventory.QualifyingInvasions[i];
const invasion = getInvasionByOid(qi.invasionId.toString());
if (!invasion) {
logger.debug(`removing QualifyingInvasions entry for unknown invasion: ${qi.invasionId.toString()}`);
inventory.QualifyingInvasions.splice(i, 1);
continue;
}
if (invasion.Completed) {
let factionSidedWith: string | undefined;
let battlePay: ICountedItem[] | undefined;
if (qi.AttackerScore >= 3) {
factionSidedWith = invasion.Faction;
battlePay = invasion.AttackerReward.countedItems;
logger.debug(`invasion pay from ${factionSidedWith}`, { battlePay });
} else if (qi.DefenderScore >= 3) {
factionSidedWith = invasion.DefenderFaction;
battlePay = invasion.DefenderReward.countedItems;
logger.debug(`invasion pay from ${factionSidedWith}`, { battlePay });
}
if (factionSidedWith) {
if (battlePay) {
// Decoupling rewards from the inbox message because it may delete itself without being read
for (const item of battlePay) {
await addItem(inventory, item.ItemType, item.ItemCount);
}
await createMessage(account._id, [
{
sndr: eFaction.find(x => x.tag == factionSidedWith)?.name ?? factionSidedWith, // TOVERIFY
msg: `/Lotus/Language/G1Quests/${factionSidedWith}_InvasionThankyouMessageBody`,
sub: `/Lotus/Language/G1Quests/${factionSidedWith}_InvasionThankyouMessageSubject`,
countedAtt: battlePay,
attVisualOnly: true,
icon:
factionSidedWith == "FC_GRINEER"
? "/Lotus/Interface/Icons/Npcs/EliteRifleLancerAvatar.png" // Source: https://www.reddit.com/r/Warframe/comments/1aj4usx/battle_pay_worth_10_plat/, https://www.youtube.com/watch?v=XhNZ6ai6BOY
: "/Lotus/Interface/Icons/Npcs/CrewmanNormal.png", // My best source for this is https://www.youtube.com/watch?v=rxrCCFm73XE around 1:37
// TOVERIFY: highPriority?
endDate: new Date(Date.now() + 86400_000) // TOVERIFY: This type of inbox message seems to automatically delete itself. We'll just delete it after 24 hours, but it's not clear if this is correct.
}
]);
}
if (invasion.Faction != "FC_INFESTATION") {
// Sided with grineer -> opposed corpus -> send zanuka (harvester)
// Sided with corpus -> opposed grineer -> send g3 (death squad)
inventory[factionSidedWith != "FC_GRINEER" ? "DeathSquadable" : "Harvestable"] = true;
// TOVERIFY: Should this happen earlier?
// TOVERIFY: Should this send an (ephemeral) email?
}
}
logger.debug(`removing QualifyingInvasions entry for completed invasion: ${qi.invasionId.toString()}`);
inventory.QualifyingInvasions.splice(i, 1);
continue;
}
++i;
}
if (inventory.LastInventorySync) {
const lastSyncDuviriMood = Math.trunc(inventory.LastInventorySync.getTimestamp().getTime() / 7200000);
const currentDuviriMood = Math.trunc(Date.now() / 7200000);
@ -224,17 +295,17 @@ export const getInventoryResponse = async (
};
}
if (config.infiniteCredits) {
if (inventory.infiniteCredits) {
inventoryResponse.RegularCredits = 999999999;
}
if (config.infinitePlatinum) {
if (inventory.infinitePlatinum) {
inventoryResponse.PremiumCreditsFree = 0;
inventoryResponse.PremiumCredits = 999999999;
}
if (config.infiniteEndo) {
if (inventory.infiniteEndo) {
inventoryResponse.FusionPoints = 999999999;
}
if (config.infiniteRegalAya) {
if (inventory.infiniteRegalAya) {
inventoryResponse.PrimeTokens = 999999999;
}
@ -264,6 +335,17 @@ export const getInventoryResponse = async (
for (const uniqueName in ExportFlavour) {
inventoryResponse.FlavourItems.push({ ItemType: uniqueName });
}
} else if (config.worldState?.baroTennoConRelay) {
[
"/Lotus/Types/Items/Events/TennoConRelay2022EarlyAccess",
"/Lotus/Types/Items/Events/TennoConRelay2023EarlyAccess",
"/Lotus/Types/Items/Events/TennoConRelay2024EarlyAccess",
"/Lotus/Types/Items/Events/TennoConRelay2025EarlyAccess"
].forEach(uniqueName => {
if (!inventoryResponse.FlavourItems.some(x => x.ItemType == uniqueName)) {
inventoryResponse.FlavourItems.push({ ItemType: uniqueName });
}
});
}
if (config.unlockAllSkins) {
@ -368,7 +450,7 @@ export const getInventoryResponse = async (
}
if (inventoryResponse.InfestedFoundry) {
applyCheatsToInfestedFoundry(inventoryResponse.InfestedFoundry);
applyCheatsToInfestedFoundry(inventory, inventoryResponse.InfestedFoundry);
}
// Set 2FA enabled so trading post can be used

View File

@ -2,7 +2,7 @@ import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory, updateCurrency, updateSlots } from "@/src/services/inventoryService";
import { RequestHandler } from "express";
import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger";
import { exhaustive } from "@/src/utils/ts-utils";
/*
loadout slots are additionally purchased slots only
@ -22,13 +22,44 @@ export const inventorySlotsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const body = JSON.parse(req.body as string) as IInventorySlotsRequest;
if (body.Bin != InventorySlot.SUITS && body.Bin != InventorySlot.PVE_LOADOUTS) {
logger.warn(`unexpected slot purchase of type ${body.Bin}, account may be overcharged`);
let price;
let amount;
switch (body.Bin) {
case InventorySlot.SUITS:
case InventorySlot.MECHSUITS:
case InventorySlot.PVE_LOADOUTS:
case InventorySlot.CREWMEMBERS:
price = 20;
amount = 1;
break;
case InventorySlot.SPACESUITS:
price = 12;
amount = 1;
break;
case InventorySlot.WEAPONS:
case InventorySlot.SPACEWEAPONS:
case InventorySlot.SENTINELS:
case InventorySlot.RJ_COMPONENT_AND_ARMAMENTS:
case InventorySlot.AMPS:
price = 12;
amount = 2;
break;
case InventorySlot.RIVENS:
price = 60;
amount = 3;
break;
default:
exhaustive(body.Bin);
throw new Error(`unexpected slot purchase of type ${body.Bin as string}`);
}
const inventory = await getInventory(accountId);
const currencyChanges = updateCurrency(inventory, 20, true);
updateSlots(inventory, body.Bin, 1, 1);
const currencyChanges = updateCurrency(inventory, price, true);
updateSlots(inventory, body.Bin, amount, amount);
await inventory.save();
res.json({ InventoryChanges: currencyChanges });

View File

@ -130,7 +130,7 @@ const createLoginResponse = (
resp.Groups = [];
}
if (version_compare(buildLabel, "2021.04.13.19.58") >= 0) {
resp.DTLS = 99;
resp.DTLS = 0; // bit 0 enables DTLS. if enabled, additional bits can be set, e.g. bit 2 to enable logging. on live, the value is 99.
}
if (version_compare(buildLabel, "2022.04.29.12.53") >= 0) {
resp.ClientType = account.ClientType;

View File

@ -6,7 +6,11 @@ import { addMissionInventoryUpdates, addMissionRewards } from "@/src/services/mi
import { getInventory } from "@/src/services/inventoryService";
import { getInventoryResponse } from "@/src/controllers/api/inventoryController";
import { logger } from "@/src/utils/logger";
import { IMissionInventoryUpdateResponse } from "@/src/types/missionTypes";
import {
IMissionInventoryUpdateResponse,
IMissionInventoryUpdateResponseBackToDryDock,
IMissionInventoryUpdateResponseRailjackInterstitial
} from "@/src/types/missionTypes";
import { sendWsBroadcastTo } from "@/src/services/wsService";
import { generateRewardSeed } from "@/src/services/rngService";
@ -95,11 +99,9 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
inventory.RewardSeed = generateRewardSeed();
}
await inventory.save();
const inventoryResponse = await getInventoryResponse(inventory, true, account.BuildLabel);
//TODO: figure out when to send inventory. it is needed for many cases.
res.json({
InventoryJson: JSON.stringify(inventoryResponse),
const deltas: IMissionInventoryUpdateResponseRailjackInterstitial = {
InventoryChanges: inventoryChanges,
MissionRewards,
...credits,
@ -108,7 +110,25 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
SyndicateXPItemReward,
AffiliationMods,
ConquestCompletedMissionsCount
} satisfies IMissionInventoryUpdateResponse);
};
if (missionReport.RJ) {
logger.debug(`railjack interstitial request, sending only deltas`, deltas);
res.json(deltas);
} else if (missionReport.RewardInfo) {
logger.debug(`classic mission completion, sending everything`);
const inventoryResponse = await getInventoryResponse(inventory, true, account.BuildLabel);
res.json({
InventoryJson: JSON.stringify(inventoryResponse),
...deltas
} satisfies IMissionInventoryUpdateResponse);
} else {
logger.debug(`no reward info, assuming this wasn't a mission completion and we should just sync inventory`);
const inventoryResponse = await getInventoryResponse(inventory, true, account.BuildLabel);
res.json({
InventoryJson: JSON.stringify(inventoryResponse)
} satisfies IMissionInventoryUpdateResponseBackToDryDock);
}
sendWsBroadcastTo(account._id.toString(), { update_inventory: true });
};

View File

@ -1,5 +1,6 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { sendWsBroadcastTo } from "@/src/services/wsService";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import {
getInventory,
@ -194,4 +195,5 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res)
MiscItems: miscItemChanges
}
});
sendWsBroadcastTo(accountId, { update_inventory: true });
};

View File

@ -15,6 +15,7 @@ import {
} from "@/src/services/inventoryService";
import { getDefaultUpgrades } from "@/src/services/itemDataService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { sendWsBroadcastTo } from "@/src/services/wsService";
import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { EquipmentFeatures } from "@/src/types/equipmentTypes";
@ -68,6 +69,7 @@ export const modularWeaponSaleController: RequestHandler = async (req, res) => {
res.json({
InventoryChanges: inventoryChanges
});
sendWsBroadcastTo(accountId, { update_inventory: true });
} else {
throw new Error(`unknown modularWeaponSale op: ${String(req.query.op)}`);
}

View File

@ -1,25 +1,52 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService";
import { addConsumables, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IPlayerSkills } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express";
export const playerSkillsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "PlayerSkills");
const inventory = await getInventory(accountId, "PlayerSkills Consumables");
const request = getJSONfromString<IPlayerSkillsRequest>(String(req.body));
const oldRank: number = inventory.PlayerSkills[request.Skill as keyof IPlayerSkills];
const cost = (request.Pool == "LPP_DRIFTER" ? drifterCosts[oldRank] : 1 << oldRank) * 1000;
inventory.PlayerSkills[request.Pool as keyof IPlayerSkills] -= cost;
inventory.PlayerSkills[request.Skill as keyof IPlayerSkills]++;
await inventory.save();
const inventoryChanges: IInventoryChanges = {};
if (request.Skill == "LPS_COMMAND") {
if (inventory.PlayerSkills.LPS_COMMAND == 9) {
const consumablesChanges = [
{
ItemType: "/Lotus/Types/Restoratives/Consumable/CrewmateBall",
ItemCount: 1
}
];
addConsumables(inventory, consumablesChanges);
inventoryChanges.Consumables = consumablesChanges;
}
} else if (request.Skill == "LPS_DRIFT_RIDING") {
if (inventory.PlayerSkills.LPS_DRIFT_RIDING == 9) {
const consumablesChanges = [
{
ItemType: "/Lotus/Types/Restoratives/ErsatzSummon",
ItemCount: 1
}
];
addConsumables(inventory, consumablesChanges);
inventoryChanges.Consumables = consumablesChanges;
}
}
await inventory.save();
res.json({
Pool: request.Pool,
PoolInc: -cost,
Skill: request.Skill,
Rank: oldRank + 1
Rank: oldRank + 1,
InventoryChanges: inventoryChanges
});
};

View File

@ -11,7 +11,7 @@ export const projectionManagerController: RequestHandler = async (req, res) => {
const [era, category, currentQuality] = parseProjection(request.projectionType);
const upgradeCost = config.dontSubtractVoidTraces
? 0
: (request.qualityTag - qualityKeywordToNumber[currentQuality]) * 25;
: qualityNumberToCost[request.qualityTag] - qualityNumberToCost[qualityKeywordToNumber[currentQuality]];
const newProjectionType = findProjection(era, category, qualityNumberToKeyword[request.qualityTag]);
addMiscItems(inventory, [
{
@ -49,6 +49,7 @@ const qualityKeywordToNumber: Record<VoidProjectionQuality, number> = {
VPQ_GOLD: 2,
VPQ_PLATINUM: 3
};
const qualityNumberToCost = [0, 25, 50, 100];
// e.g. "/Lotus/Types/Game/Projections/T2VoidProjectionProteaPrimeDBronze" -> ["Lith", "W5", "VPQ_BRONZE"]
const parseProjection = (typeName: string): [string, string, VoidProjectionQuality] => {

View File

@ -1,6 +1,7 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory, updateCurrency } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { sendWsBroadcastTo } from "@/src/services/wsService";
import { RequestHandler } from "express";
export const releasePetController: RequestHandler = async (req, res) => {
@ -19,6 +20,7 @@ export const releasePetController: RequestHandler = async (req, res) => {
await inventory.save();
res.json({ inventoryChanges }); // Not a mistake; it's "inventoryChanges" here.
sendWsBroadcastTo(accountId, { update_inventory: true });
};
interface IReleasePetRequest {

View File

@ -0,0 +1,5 @@
import { RequestHandler } from "express";
export const resetQuestProgressController: RequestHandler = (_req, res) => {
res.send("1").end();
};

View File

@ -9,13 +9,16 @@ import {
freeUpSlot,
combineInventoryChanges,
addCrewShipRawSalvage,
addFusionPoints
addFusionPoints,
addCrewShipFusionPoints,
addFusionTreasures
} from "@/src/services/inventoryService";
import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { ExportDojoRecipes } from "warframe-public-export-plus";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { sendWsBroadcastTo } from "@/src/services/wsService";
import { sendWsBroadcastEx } from "@/src/services/wsService";
import { parseFusionTreasure } from "@/src/helpers/inventoryHelpers";
export const sellController: RequestHandler = async (req, res) => {
const payload = JSON.parse(String(req.body)) as ISellRequest;
@ -26,6 +29,8 @@ export const sellController: RequestHandler = async (req, res) => {
requiredFields.add("RegularCredits");
} else if (payload.SellCurrency == "SC_FusionPoints") {
requiredFields.add("FusionPoints");
} else if (payload.SellCurrency == "SC_CrewShipFusionPoints") {
requiredFields.add("CrewShipFusionPoints");
} else {
requiredFields.add("MiscItems");
}
@ -79,6 +84,8 @@ export const sellController: RequestHandler = async (req, res) => {
inventory.RegularCredits += payload.SellPrice;
} else if (payload.SellCurrency == "SC_FusionPoints") {
addFusionPoints(inventory, payload.SellPrice);
} else if (payload.SellCurrency == "SC_CrewShipFusionPoints") {
addCrewShipFusionPoints(inventory, payload.SellPrice);
} else if (payload.SellCurrency == "SC_PrimeBucks") {
addMiscItems(inventory, [
{
@ -290,12 +297,17 @@ export const sellController: RequestHandler = async (req, res) => {
]);
});
}
if (payload.Items.FusionTreasures) {
payload.Items.FusionTreasures.forEach(sellItem => {
addFusionTreasures(inventory, [parseFusionTreasure(sellItem.String, sellItem.Count * -1)]);
});
}
await inventory.save();
res.json({
inventoryChanges: inventoryChanges // "inventoryChanges" for this response instead of the usual "InventoryChanges"
});
sendWsBroadcastTo(accountId, { update_inventory: true });
sendWsBroadcastEx({ update_inventory: true }, accountId, parseInt(String(req.query.wsid)));
};
interface ISellRequest {
@ -322,6 +334,7 @@ interface ISellRequest {
CrewMembers?: ISellItem[];
CrewShipWeapons?: ISellItem[];
CrewShipWeaponSkins?: ISellItem[];
FusionTreasures?: ISellItem[];
};
SellPrice: number;
SellCurrency:
@ -330,7 +343,8 @@ interface ISellRequest {
| "SC_FusionPoints"
| "SC_DistillPoints"
| "SC_CrewShipFusionPoints"
| "SC_Resources";
| "SC_Resources"
| "somethingelsewemightnotknowabout";
buildLabel: string;
}

View File

@ -1,23 +1,19 @@
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IPictureFrameInfo, ISetPlacedDecoInfoRequest } from "@/src/types/personalRoomsTypes";
import { ISetPlacedDecoInfoRequest } from "@/src/types/personalRoomsTypes";
import { RequestHandler } from "express";
import { handleSetPlacedDecoInfo } from "@/src/services/shipCustomizationsService";
export const setPlacedDecoInfoController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const payload = JSON.parse(req.body as string) as ISetPlacedDecoInfoRequest;
//console.log(JSON.stringify(payload, null, 2));
await handleSetPlacedDecoInfo(accountId, payload);
res.json({
DecoId: payload.DecoId,
IsPicture: true,
PictureFrameInfo: payload.PictureFrameInfo,
BootLocation: payload.BootLocation
...payload,
IsPicture: !!payload.PictureFrameInfo
} satisfies ISetPlacedDecoInfoResponse);
};
interface ISetPlacedDecoInfoResponse {
DecoId: string;
interface ISetPlacedDecoInfoResponse extends ISetPlacedDecoInfoRequest {
IsPicture: boolean;
PictureFrameInfo?: IPictureFrameInfo;
BootLocation?: string;
}

View File

@ -1,20 +1,17 @@
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IShipDecorationsRequest } from "@/src/types/personalRoomsTypes";
import { logger } from "@/src/utils/logger";
import { IShipDecorationsRequest, IResetShipDecorationsRequest } from "@/src/types/personalRoomsTypes";
import { RequestHandler } from "express";
import { handleSetShipDecorations } from "@/src/services/shipCustomizationsService";
import { handleResetShipDecorations, handleSetShipDecorations } from "@/src/services/shipCustomizationsService";
export const shipDecorationsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const shipDecorationsRequest = JSON.parse(req.body as string) as IShipDecorationsRequest;
try {
if (req.query.reset == "1") {
const request = JSON.parse(req.body as string) as IResetShipDecorationsRequest;
const response = await handleResetShipDecorations(accountId, request);
res.send(response);
} else {
const shipDecorationsRequest = JSON.parse(req.body as string) as IShipDecorationsRequest;
const placedDecoration = await handleSetShipDecorations(accountId, shipDecorationsRequest);
res.send(placedDecoration);
} catch (error: unknown) {
if (error instanceof Error) {
logger.error(`error in shipDecorationsController: ${error.message}`);
res.status(400).json({ error: error.message });
}
}
};

View File

@ -10,6 +10,7 @@ import { logger } from "@/src/utils/logger";
export const updateChallengeProgressController: RequestHandler = async (req, res) => {
const challenges = getJSONfromString<IUpdateChallengeProgressRequest>(String(req.body));
const account = await getAccountForRequest(req);
logger.debug(`challenge report:`, challenges);
const inventory = await getInventory(
account._id.toString(),
@ -17,7 +18,7 @@ export const updateChallengeProgressController: RequestHandler = async (req, res
);
let affiliationMods: IAffiliationMods[] = [];
if (challenges.ChallengeProgress) {
affiliationMods = addChallenges(
affiliationMods = await addChallenges(
account,
inventory,
challenges.ChallengeProgress,

View File

@ -7,7 +7,6 @@ import { addMiscItems, addRecipes, getInventory, updateCurrency } from "@/src/se
import { getRecipeByResult } from "@/src/services/itemDataService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { addInfestedFoundryXP, applyCheatsToInfestedFoundry } from "@/src/services/infestedFoundryService";
import { config } from "@/src/services/configService";
import { sendWsBroadcastTo } from "@/src/services/wsService";
import { EquipmentFeatures, IEquipmentDatabase } from "@/src/types/equipmentTypes";
@ -52,7 +51,7 @@ export const upgradesController: RequestHandler = async (req, res) => {
const recipe = getRecipeByResult(operation.UpgradeRequirement)!;
for (const ingredient of recipe.ingredients) {
totalPercentagePointsConsumed += ingredient.ItemCount / 10;
if (!config.infiniteHelminthMaterials) {
if (!inventory.infiniteHelminthMaterials) {
inventory.InfestedFoundry!.Resources!.find(x => x.ItemType == ingredient.ItemType)!.Count -=
ingredient.ItemCount;
}
@ -69,7 +68,7 @@ export const upgradesController: RequestHandler = async (req, res) => {
inventoryChanges.Recipes = recipeChanges;
inventoryChanges.InfestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry;
applyCheatsToInfestedFoundry(inventoryChanges.InfestedFoundry!);
applyCheatsToInfestedFoundry(inventory, inventoryChanges.InfestedFoundry!);
} else
switch (operation.UpgradeRequirement) {
case "/Lotus/Types/Items/MiscItems/OrokinReactor":

View File

@ -0,0 +1,33 @@
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
export const abilityOverrideController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const request = req.body as IAbilityOverrideRequest;
if (request.category === "Suits") {
const inventory = await getInventory(accountId, request.category);
const item = inventory[request.category].id(request.oid);
if (item) {
if (request.action == "set") {
item.Configs[request.configIndex].AbilityOverride = request.AbilityOverride;
} else {
item.Configs[request.configIndex].AbilityOverride = undefined;
}
await inventory.save();
}
}
res.end();
};
interface IAbilityOverrideRequest {
category: TEquipmentKey;
oid: string;
action: "set" | "remove";
configIndex: number;
AbilityOverride: {
Ability: string;
Index: number;
};
}

View File

@ -0,0 +1,65 @@
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
export const changeModularPartsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const request = req.body as IUpdateFingerPrintRequest;
const inventory = await getInventory(accountId, request.category);
const item = inventory[request.category].id(request.oid);
if (item) {
item.ModularParts = request.modularParts;
request.modularParts.forEach(part => {
const categoryMap = mapping[part];
if (categoryMap && categoryMap[request.category]) {
item.ItemType = categoryMap[request.category]!;
}
});
await inventory.save();
}
res.end();
};
interface IUpdateFingerPrintRequest {
category: TEquipmentKey;
oid: string;
modularParts: string[];
}
const mapping: Partial<Record<string, Partial<Record<TEquipmentKey, string>>>> = {
"/Lotus/Weapons/SolarisUnited/Secondary/SUModularSecondarySet1/Barrel/SUModularSecondaryBarrelAPart": {
LongGuns: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryShotgun",
Pistols: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun"
},
"/Lotus/Weapons/Infested/Pistols/InfKitGun/Barrels/InfBarrelEgg/InfModularBarrelEggPart": {
LongGuns: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryShotgun",
Pistols: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun"
},
"/Lotus/Weapons/SolarisUnited/Secondary/SUModularSecondarySet1/Barrel/SUModularSecondaryBarrelBPart": {
LongGuns: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary",
Pistols: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary"
},
"/Lotus/Weapons/SolarisUnited/Secondary/SUModularSecondarySet1/Barrel/SUModularSecondaryBarrelCPart": {
LongGuns: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary",
Pistols: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary"
},
"/Lotus/Weapons/SolarisUnited/Secondary/SUModularSecondarySet1/Barrel/SUModularSecondaryBarrelDPart": {
LongGuns: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam",
Pistols: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam"
},
"/Lotus/Weapons/Infested/Pistols/InfKitGun/Barrels/InfBarrelBeam/InfModularBarrelBeamPart": {
LongGuns: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam",
Pistols: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam"
},
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadA": {
MoaPets: "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetAPowerSuit"
},
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadB": {
MoaPets: "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetBPowerSuit"
},
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadC": {
MoaPets: "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit"
}
};

View File

@ -1,8 +1,8 @@
import { RequestHandler } from "express";
import { config } from "@/src/services/configService";
import { config, syncConfigWithDatabase } from "@/src/services/configService";
import { getAccountForRequest, isAdministrator } from "@/src/services/loginService";
import { saveConfig } from "@/src/services/configWriterService";
import { sendWsBroadcastExcept } from "@/src/services/wsService";
import { sendWsBroadcastEx } from "@/src/services/wsService";
export const getConfigController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req);
@ -25,7 +25,8 @@ export const setConfigController: RequestHandler = async (req, res) => {
const [obj, idx] = configIdToIndexable(id);
obj[idx] = value;
}
sendWsBroadcastExcept(parseInt(String(req.query.wsid)), { config_reloaded: true });
sendWsBroadcastEx({ config_reloaded: true }, undefined, parseInt(String(req.query.wsid)));
syncConfigWithDatabase();
await saveConfig();
res.end();
} else {
@ -37,6 +38,8 @@ const configIdToIndexable = (id: string): [Record<string, boolean | string | num
let obj = config as unknown as Record<string, never>;
const arr = id.split(".");
while (arr.length > 1) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
obj[arr[0]] ??= {} as never;
obj = obj[arr[0]];
arr.splice(0, 1);
}

View File

@ -11,6 +11,7 @@ import { GuildMember } from "@/src/models/guildModel";
import { Leaderboard } from "@/src/models/leaderboardModel";
import { deleteGuild } from "@/src/services/guildService";
import { Friendship } from "@/src/models/friendModel";
import { sendWsBroadcastTo } from "@/src/services/wsService";
export const deleteAccountController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -36,5 +37,8 @@ export const deleteAccountController: RequestHandler = async (req, res) => {
Ship.deleteMany({ ShipOwnerId: accountId }),
Stats.deleteOne({ accountOwnerId: accountId })
]);
sendWsBroadcastTo(accountId, { logged_out: true });
res.end();
};

View File

@ -0,0 +1,34 @@
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService";
import { RequestHandler } from "express";
const DEFAULT_UPGRADE_EXPIRY_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
export const editSuitInvigorationUpgradeController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const { oid, data } = req.body as {
oid: string;
data?: {
DefensiveUpgrade: string;
OffensiveUpgrade: string;
UpgradesExpiry?: number;
};
};
const inventory = await getInventory(accountId);
const suit = inventory.Suits.id(oid)!;
if (data) {
suit.DefensiveUpgrade = data.DefensiveUpgrade;
suit.OffensiveUpgrade = data.OffensiveUpgrade;
if (data.UpgradesExpiry) {
suit.UpgradesExpiry = new Date(data.UpgradesExpiry);
} else {
suit.UpgradesExpiry = new Date(Date.now() + DEFAULT_UPGRADE_EXPIRY_MS);
}
} else {
suit.DefensiveUpgrade = undefined;
suit.OffensiveUpgrade = undefined;
suit.UpgradesExpiry = undefined;
}
await inventory.save();
res.end();
};

View File

@ -1,6 +1,7 @@
import { RequestHandler } from "express";
import { getDict, getItemName, getString } from "@/src/services/itemDataService";
import {
ExportAbilities,
ExportArcanes,
ExportAvionics,
ExportBoosters,
@ -57,6 +58,7 @@ interface ItemLists {
mods: ListedItem[];
Boosters: ListedItem[];
VarziaOffers: ListedItem[];
Abilities: ListedItem[];
//circuitGameModes: ListedItem[];
}
@ -94,7 +96,8 @@ const getItemListsController: RequestHandler = (req, response) => {
EvolutionProgress: [],
mods: [],
Boosters: [],
VarziaOffers: []
VarziaOffers: [],
Abilities: []
/*circuitGameModes: [
{
uniqueName: "Survival",
@ -132,6 +135,12 @@ const getItemListsController: RequestHandler = (req, response) => {
name: getString(item.name, lang),
exalted: item.exalted
});
item.abilities.forEach(ability => {
res.Abilities.push({
uniqueName: ability.uniqueName,
name: getString(ability.name || uniqueName, lang)
});
});
}
for (const [uniqueName, item] of Object.entries(ExportSentinels)) {
if (item.productCategory == "Sentinels" || item.productCategory == "KubrowPets") {
@ -144,18 +153,21 @@ const getItemListsController: RequestHandler = (req, response) => {
}
for (const [uniqueName, item] of Object.entries(ExportWeapons)) {
if (item.partType) {
if (!uniqueName.startsWith("/Lotus/Types/Items/Deimos/")) {
res.ModularParts.push({
uniqueName,
name: getString(item.name, lang),
partType: item.partType
});
}
if (uniqueName.split("/")[5] != "SentTrainingAmplifier") {
res.miscitems.push({
uniqueName: uniqueName,
name: getString(item.name, lang)
});
if (!uniqueName.split("/")[7]?.startsWith("PvPVariant")) {
// not a pvp variant
if (!uniqueName.startsWith("/Lotus/Types/Items/Deimos/")) {
res.ModularParts.push({
uniqueName,
name: getString(item.name, lang),
partType: item.partType
});
}
if (uniqueName.split("/")[5] != "SentTrainingAmplifier") {
res.miscitems.push({
uniqueName: uniqueName,
name: getString(item.name, lang)
});
}
}
} else if (item.totalDamage !== 0) {
if (
@ -348,6 +360,13 @@ const getItemListsController: RequestHandler = (req, response) => {
});
}
for (const [uniqueName, ability] of Object.entries(ExportAbilities)) {
res.Abilities.push({
uniqueName,
name: getString(ability.name || uniqueName, lang)
});
}
response.json(res);
};

View File

@ -1,8 +1,10 @@
import { importInventory, importLoadOutPresets } from "@/src/services/importService";
import { importInventory, importLoadOutPresets, importPersonalRooms } from "@/src/services/importService";
import { getInventory } from "@/src/services/inventoryService";
import { getLoadout } from "@/src/services/loadoutService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getPersonalRooms } from "@/src/services/personalRoomsService";
import { IInventoryClient } from "@/src/types/inventoryTypes/inventoryTypes";
import { IGetShipResponse } from "@/src/types/personalRoomsTypes";
import { RequestHandler } from "express";
export const importController: RequestHandler = async (req, res) => {
@ -13,15 +15,21 @@ export const importController: RequestHandler = async (req, res) => {
importInventory(inventory, request.inventory);
await inventory.save();
if (request.inventory.LoadOutPresets) {
if ("LoadOutPresets" in request.inventory && request.inventory.LoadOutPresets) {
const loadout = await getLoadout(accountId);
importLoadOutPresets(loadout, request.inventory.LoadOutPresets);
await loadout.save();
}
if ("Ship" in request.inventory || "Apartment" in request.inventory || "TailorShop" in request.inventory) {
const personalRooms = await getPersonalRooms(accountId);
importPersonalRooms(personalRooms, request.inventory);
await personalRooms.save();
}
res.end();
};
interface IImportRequest {
inventory: Partial<IInventoryClient>;
inventory: Partial<IInventoryClient> | Partial<IGetShipResponse>;
}

View File

@ -0,0 +1,18 @@
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IAccountCheats } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
export const setAccountCheatController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const payload = req.body as ISetAccountCheatRequest;
const inventory = await getInventory(accountId, payload.key);
inventory[payload.key] = payload.value;
await inventory.save();
res.end();
};
interface ISetAccountCheatRequest {
key: keyof IAccountCheats;
value: boolean;
}

View File

@ -141,7 +141,7 @@ export const getProfileViewingDataGetController: RequestHandler = async (req, re
}
}
} else {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-explicit-any
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
combinedStats[arrayName].push(entry as any);
}
}

View File

@ -1,6 +1,7 @@
import { IMongoDate, IOid, IOidWithLegacySupport } from "@/src/types/commonTypes";
import { Types } from "mongoose";
import { TRarity } from "warframe-public-export-plus";
import { IFusionTreasure } from "@/src/types/inventoryTypes/inventoryTypes";
export const version_compare = (a: string, b: string): number => {
const a_digits = a
@ -51,6 +52,20 @@ export const fromMongoDate = (date: IMongoDate): Date => {
return new Date(parseInt(date.$date.$numberLong));
};
export const parseFusionTreasure = (name: string, count: number): IFusionTreasure => {
const arr = name.split("_");
return {
ItemType: arr[0],
Sockets: parseInt(arr[1], 16),
ItemCount: count
};
};
export type TTraitsPool = Record<
"Colors" | "EyeColors" | "FurPatterns" | "BodyTypes" | "Heads" | "Tails",
{ type: string; rarity: TRarity }[]
>;
export const kubrowWeights: Record<TRarity, number> = {
COMMON: 6,
UNCOMMON: 4,
@ -65,126 +80,126 @@ export const kubrowFurPatternsWeights: Record<TRarity, number> = {
LEGENDARY: 1
};
export const catbrowDetails = {
export const catbrowDetails: TTraitsPool = {
Colors: [
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseA", rarity: "COMMON" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseB", rarity: "COMMON" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseC", rarity: "COMMON" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseD", rarity: "COMMON" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseA", rarity: "COMMON" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseB", rarity: "COMMON" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseC", rarity: "COMMON" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseD", rarity: "COMMON" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryA", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryB", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryC", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryD", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryA", rarity: "UNCOMMON" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryB", rarity: "UNCOMMON" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryC", rarity: "UNCOMMON" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryD", rarity: "UNCOMMON" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryA", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryB", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryC", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryD", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryA", rarity: "RARE" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryB", rarity: "RARE" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryC", rarity: "RARE" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryD", rarity: "RARE" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsA", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsB", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsC", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsD", rarity: "LEGENDARY" as TRarity }
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsA", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsB", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsC", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsD", rarity: "LEGENDARY" }
],
EyeColors: [
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesA", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesB", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesC", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesD", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesE", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesF", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesG", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesH", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesI", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesJ", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesK", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesL", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesM", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesN", rarity: "LEGENDARY" as TRarity }
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesA", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesB", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesC", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesD", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesE", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesF", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesG", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesH", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesI", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesJ", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesK", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesL", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesM", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesN", rarity: "LEGENDARY" }
],
FurPatterns: [{ type: "/Lotus/Types/Game/CatbrowPet/Patterns/CatbrowPetPatternA", rarity: "COMMON" as TRarity }],
FurPatterns: [{ type: "/Lotus/Types/Game/CatbrowPet/Patterns/CatbrowPetPatternA", rarity: "COMMON" }],
BodyTypes: [
{ type: "/Lotus/Types/Game/CatbrowPet/BodyTypes/CatbrowPetRegularBodyType", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/BodyTypes/CatbrowPetRegularBodyType", rarity: "LEGENDARY" as TRarity }
{ type: "/Lotus/Types/Game/CatbrowPet/BodyTypes/CatbrowPetRegularBodyType", rarity: "UNCOMMON" },
{ type: "/Lotus/Types/Game/CatbrowPet/BodyTypes/CatbrowPetRegularBodyType", rarity: "LEGENDARY" }
],
Heads: [
{ type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadA", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadB", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadC", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadD", rarity: "LEGENDARY" as TRarity }
{ type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadA", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadB", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadC", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadD", rarity: "LEGENDARY" }
],
Tails: [
{ type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailA", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailB", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailC", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailD", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailE", rarity: "LEGENDARY" as TRarity }
{ type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailA", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailB", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailC", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailD", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailE", rarity: "LEGENDARY" }
]
};
export const kubrowDetails = {
export const kubrowDetails: TTraitsPool = {
Colors: [
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneA", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneB", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneC", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneD", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneE", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneF", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneG", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneH", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneA", rarity: "UNCOMMON" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneB", rarity: "UNCOMMON" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneC", rarity: "UNCOMMON" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneD", rarity: "UNCOMMON" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneE", rarity: "UNCOMMON" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneF", rarity: "UNCOMMON" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneG", rarity: "UNCOMMON" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneH", rarity: "UNCOMMON" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidA", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidB", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidC", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidD", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidE", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidF", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidG", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidH", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidA", rarity: "RARE" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidB", rarity: "RARE" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidC", rarity: "RARE" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidD", rarity: "RARE" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidE", rarity: "RARE" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidF", rarity: "RARE" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidG", rarity: "RARE" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidH", rarity: "RARE" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantA", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantB", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantC", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantD", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantE", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantF", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantG", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantH", rarity: "LEGENDARY" as TRarity }
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantA", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantB", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantC", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantD", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantE", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantF", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantG", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantH", rarity: "LEGENDARY" }
],
EyeColors: [
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesA", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesB", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesC", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesD", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesE", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesF", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesG", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesH", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesI", rarity: "LEGENDARY" as TRarity }
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesA", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesB", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesC", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesD", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesE", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesF", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesG", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesH", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesI", rarity: "LEGENDARY" }
],
FurPatterns: [
{ type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternB", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternA", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternB", rarity: "UNCOMMON" },
{ type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternA", rarity: "UNCOMMON" },
{ type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternC", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternD", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternC", rarity: "RARE" },
{ type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternD", rarity: "RARE" },
{ type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternE", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternF", rarity: "LEGENDARY" as TRarity }
{ type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternE", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternF", rarity: "LEGENDARY" }
],
BodyTypes: [
{ type: "/Lotus/Types/Game/KubrowPet/BodyTypes/KubrowPetRegularBodyType", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/BodyTypes/KubrowPetHeavyBodyType", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/BodyTypes/KubrowPetThinBodyType", rarity: "LEGENDARY" as TRarity }
{ type: "/Lotus/Types/Game/KubrowPet/BodyTypes/KubrowPetRegularBodyType", rarity: "UNCOMMON" },
{ type: "/Lotus/Types/Game/KubrowPet/BodyTypes/KubrowPetHeavyBodyType", rarity: "LEGENDARY" },
{ type: "/Lotus/Types/Game/KubrowPet/BodyTypes/KubrowPetThinBodyType", rarity: "LEGENDARY" }
],
Heads: [],

View File

@ -22,8 +22,11 @@ export interface INemesisManifest {
ephemeraTypes?: Record<TInnateDamageTag, string>;
firstKillReward: string;
firstConvertReward: string;
messageTitle: string;
messageBody: string;
killMessageSubject: string;
killMessageBody: string;
convertMessageSubject: string;
convertMessageBody: string;
convertMessageIcon: string;
minBuild: string;
}
@ -57,8 +60,11 @@ class KuvaLichManifest implements INemesisManifest {
};
firstKillReward = "/Lotus/StoreItems/Upgrades/Skins/Clan/LichKillerBadgeItem";
firstConvertReward = "/Lotus/StoreItems/Upgrades/Skins/Sigils/KuvaLichSigil";
messageTitle = "/Lotus/Language/Inbox/VanquishKuvaMsgTitle";
messageBody = "/Lotus/Language/Inbox/VanquishLichMsgBody";
killMessageSubject = "/Lotus/Language/Inbox/VanquishKuvaMsgTitle";
killMessageBody = "/Lotus/Language/Inbox/VanquishLichMsgBody";
convertMessageSubject = "/Lotus/Language/Kingpins/InboxKuvaConvertedSubject";
convertMessageBody = "/Lotus/Language/Kingpins/InboxKuvaConvertedBody";
convertMessageIcon = "/Lotus/Interface/Graphics/WorldStatePanel/Grineer.png";
minBuild = "2019.10.31.22.42"; // 26.0.0
}
@ -131,8 +137,11 @@ class LawyerManifest implements INemesisManifest {
};
firstKillReward = "/Lotus/StoreItems/Upgrades/Skins/Clan/CorpusLichBadgeItem";
firstConvertReward = "/Lotus/StoreItems/Upgrades/Skins/Sigils/CorpusLichSigil";
messageTitle = "/Lotus/Language/Inbox/VanquishLawyerMsgTitle";
messageBody = "/Lotus/Language/Inbox/VanquishLichMsgBody";
killMessageSubject = "/Lotus/Language/Inbox/VanquishLawyerMsgTitle";
killMessageBody = "/Lotus/Language/Inbox/VanquishLichMsgBody";
convertMessageSubject = "/Lotus/Language/Kingpins/InboxSisterConvertedSubject";
convertMessageBody = "/Lotus/Language/Kingpins/InboxSisterConvertedBody";
convertMessageIcon = "/Lotus/Interface/Graphics/WorldStatePanel/Corpus.png";
minBuild = "2021.07.05.17.03"; // 30.5.0
}
@ -166,8 +175,11 @@ class InfestedLichManfest implements INemesisManifest {
ephemeraChance = 0;
firstKillReward = "/Lotus/StoreItems/Upgrades/Skins/Sigils/InfLichVanquishedSigil";
firstConvertReward = "/Lotus/StoreItems/Upgrades/Skins/Sigils/InfLichConvertedSigil";
messageTitle = "/Lotus/Language/Inbox/VanquishBandMsgTitle";
messageBody = "/Lotus/Language/Inbox/VanquishBandMsgBody";
killMessageSubject = "/Lotus/Language/Inbox/VanquishBandMsgTitle";
killMessageBody = "/Lotus/Language/Inbox/VanquishBandMsgBody";
convertMessageSubject = "/Lotus/Language/Kingpins/InboxBandConvertedSubject";
convertMessageBody = "/Lotus/Language/Kingpins/InboxBandConvertedBody";
convertMessageIcon = "/Lotus/Interface/Graphics/WorldStatePanel/Infested.png";
minBuild = "2025.03.18.09.51"; // 38.5.0
}

View File

@ -1,5 +1,17 @@
import { slotPurchaseNameToSlotName } from "@/src/services/purchaseService";
import { SlotPurchaseName } from "@/src/types/purchaseTypes";
import { SlotPurchase, SlotPurchaseName } from "@/src/types/purchaseTypes";
export const slotPurchaseNameToSlotName: SlotPurchase = {
SuitSlotItem: { name: "SuitBin", purchaseQuantity: 1 },
TwoSentinelSlotItem: { name: "SentinelBin", purchaseQuantity: 2 },
TwoWeaponSlotItem: { name: "WeaponBin", purchaseQuantity: 2 },
SpaceSuitSlotItem: { name: "SpaceSuitBin", purchaseQuantity: 1 },
TwoSpaceWeaponSlotItem: { name: "SpaceWeaponBin", purchaseQuantity: 2 },
MechSlotItem: { name: "MechBin", purchaseQuantity: 1 },
TwoOperatorWeaponSlotItem: { name: "OperatorAmpBin", purchaseQuantity: 2 },
RandomModSlotItem: { name: "RandomModBin", purchaseQuantity: 3 },
TwoCrewShipSalvageSlotItem: { name: "CrewShipSalvageBin", purchaseQuantity: 2 },
CrewMemberSlotItem: { name: "CrewMemberBin", purchaseQuantity: 1 }
};
export const isSlotPurchaseName = (slotPurchaseName: string): slotPurchaseName is SlotPurchaseName => {
return slotPurchaseName in slotPurchaseNameToSlotName;

View File

@ -23,10 +23,16 @@ export const crackRelic = async (
weights = { COMMON: 0, UNCOMMON: 0, RARE: 1, LEGENDARY: 0 };
}
logger.debug(`opening a relic of quality ${relic.quality}; rarity weights are`, weights);
const reward = getRandomWeightedReward(
let reward = getRandomWeightedReward(
ExportRewards[relic.rewardManifest][0] as { type: string; itemCount: number; rarity: TRarity }[], // rarity is nullable in PE+ typings, but always present for relics
weights
)!;
if (config.relicRewardItemCountMultiplier !== undefined && (config.relicRewardItemCountMultiplier ?? 1) != 1) {
reward = {
...reward,
itemCount: reward.itemCount * config.relicRewardItemCountMultiplier
};
}
logger.debug(`relic rolled`, reward);
participant.Reward = reward.type;
@ -43,13 +49,7 @@ export const crackRelic = async (
// Give reward
combineInventoryChanges(
inventoryChanges,
(
await handleStoreItemAcquisition(
reward.type,
inventory,
reward.itemCount * (config.relicRewardItemCountMultiplier ?? 1)
)
).InventoryChanges
(await handleStoreItemAcquisition(reward.type, inventory, reward.itemCount)).InventoryChanges
);
return reward;

View File

@ -1,5 +1,5 @@
// First, init config.
import { config, configPath, loadConfig } from "@/src/services/configService";
import { config, configPath, loadConfig, syncConfigWithDatabase } from "@/src/services/configService";
import fs from "fs";
try {
loadConfig();
@ -7,7 +7,7 @@ try {
if (fs.existsSync("config.json")) {
console.log("Failed to load " + configPath + ": " + (e as Error).message);
} else {
console.log("Failed to load " + configPath + ". You can copy config.json.example to create your config file.");
console.log("Failed to load " + configPath + ". You can copy config-vanilla.json to create your config file.");
}
process.exit(1);
}
@ -18,17 +18,23 @@ logger.info("Starting up...");
// Proceed with normal startup: bring up config watcher service, validate config, connect to MongoDB, and finally start listening for HTTP.
import mongoose from "mongoose";
import path from "path";
import { JSONStringify } from "json-with-bigint";
import { startWebServer } from "@/src/services/webService";
import { syncConfigWithDatabase, validateConfig } from "@/src/services/configWatcherService";
import { validateConfig } from "@/src/services/configWatcherService";
import { updateWorldStateCollections } from "@/src/services/worldStateService";
import { repoDir } from "@/src/helpers/pathHelper";
// Patch JSON.stringify to work flawlessly with Bigints.
JSON.stringify = JSONStringify;
JSON.stringify = JSONStringify; // Patch JSON.stringify to work flawlessly with Bigints.
validateConfig();
fs.readFile(path.join(repoDir, "BUILD_DATE"), "utf-8", (err, data) => {
if (!err) {
logger.info(`Docker image was built on ${data.trim()}`);
}
});
mongoose
.connect(config.mongodbUrl)
.then(() => {

View File

@ -1,16 +1,11 @@
import { NextFunction, Request, Response } from "express";
import { logger } from "@/src/utils/logger";
import { logError } from "@/src/utils/logger";
export const errorHandler = (err: Error, req: Request, res: Response, _next: NextFunction): void => {
if (err.message == "Invalid accountId-nonce pair") {
res.status(400).send("Log-in expired");
} else if (err.stack) {
const stackArr = err.stack.split("\n");
stackArr[0] += ` while processing ${req.path} request`;
logger.error(stackArr.join("\n"));
res.status(500).end();
} else {
logger.error(`uncaught error while processing ${req.path} request: ${err.message}`);
logError(err, `processing ${req.path} request`);
res.status(500).end();
}
};

View File

@ -19,6 +19,8 @@ import {
import { Document, Model, model, Schema, Types } from "mongoose";
import { fusionTreasuresSchema, typeCountSchema } from "@/src/models/inventoryModels/inventoryModel";
import { pictureFrameInfoSchema } from "@/src/models/personalRoomsModel";
import { IGoalProgressClient, IGoalProgressDatabase } from "@/src/types/inventoryTypes/inventoryTypes";
import { toOid } from "@/src/helpers/inventoryHelpers";
const dojoDecoSchema = new Schema<IDojoDecoDatabase>({
Type: String,
@ -174,6 +176,28 @@ const guildLogEntryNumberSchema = new Schema<IGuildLogEntryNumber>(
{ _id: false }
);
const goalProgressSchema = new Schema<IGoalProgressDatabase>(
{
Count: Number,
Tag: String,
goalId: Types.ObjectId
},
{ _id: false }
);
goalProgressSchema.set("toJSON", {
virtuals: true,
transform(_doc, obj: Record<string, any>) {
const db = obj as IGoalProgressDatabase;
const client = obj as IGoalProgressClient;
client._id = toOid(db.goalId);
delete obj.goalId;
delete obj.__v;
}
});
const guildSchema = new Schema<IGuildDatabase>(
{
Name: { type: String, required: true, unique: true },
@ -206,7 +230,8 @@ const guildSchema = new Schema<IGuildDatabase>(
RoomChanges: { type: [guildLogRoomChangeSchema], default: undefined },
TechChanges: { type: [guildLogEntryContributableSchema], default: undefined },
RosterActivity: { type: [guildLogEntryRosterSchema], default: undefined },
ClassChanges: { type: [guildLogEntryNumberSchema], default: undefined }
ClassChanges: { type: [guildLogEntryNumberSchema], default: undefined },
GoalProgress: { type: [goalProgressSchema], default: undefined }
},
{ id: false }
);

View File

@ -4,8 +4,12 @@ import { typeCountSchema } from "@/src/models/inventoryModels/inventoryModel";
import { IMongoDate, IOid, ITypeCount } from "@/src/types/commonTypes";
export interface IMessageClient
extends Omit<IMessageDatabase, "_id" | "date" | "startDate" | "endDate" | "ownerId" | "attVisualOnly" | "expiry"> {
extends Omit<
IMessageDatabase,
"_id" | "globaUpgradeId" | "date" | "startDate" | "endDate" | "ownerId" | "attVisualOnly" | "expiry"
> {
_id?: IOid;
globaUpgradeId?: IOid; // [sic]
date: IMongoDate;
startDate?: IMongoDate;
endDate?: IMongoDate;
@ -14,6 +18,7 @@ export interface IMessageClient
export interface IMessageDatabase extends IMessage {
ownerId: Types.ObjectId;
globaUpgradeId?: Types.ObjectId; // [sic]
date: Date; //created at
attVisualOnly?: boolean;
_id: Types.ObjectId;
@ -42,6 +47,7 @@ export interface IMessage {
acceptAction?: string;
declineAction?: string;
hasAccountAction?: boolean;
RegularCredits?: number;
}
export interface Arg {
@ -101,6 +107,7 @@ const giftSchema = new Schema<IGift>(
const messageSchema = new Schema<IMessageDatabase>(
{
ownerId: Schema.Types.ObjectId,
globaUpgradeId: Schema.Types.ObjectId,
sndr: String,
msg: String,
cinematic: String,
@ -133,7 +140,8 @@ const messageSchema = new Schema<IMessageDatabase>(
contextInfo: String,
acceptAction: String,
declineAction: String,
hasAccountAction: Boolean
hasAccountAction: Boolean,
RegularCredits: Number
},
{ id: false }
);
@ -144,7 +152,7 @@ messageSchema.virtual("messageId").get(function (this: IMessageDatabase) {
messageSchema.set("toJSON", {
virtuals: true,
transform(_document, returnedObject) {
transform(_document, returnedObject: Record<string, any>) {
const messageDatabase = returnedObject as IMessageDatabase;
const messageClient = returnedObject as IMessageClient;
@ -154,6 +162,10 @@ messageSchema.set("toJSON", {
delete returnedObject.attVisualOnly;
delete returnedObject.expiry;
if (messageDatabase.globaUpgradeId) {
messageClient.globaUpgradeId = toOid(messageDatabase.globaUpgradeId);
}
messageClient.date = toMongoDate(messageDatabase.date);
if (messageDatabase.startDate && messageDatabase.endDate) {

View File

@ -85,8 +85,8 @@ import {
IAccolades,
IHubNpcCustomization,
IEndlessXpReward,
IPersonalGoalProgressDatabase,
IPersonalGoalProgressClient,
IGoalProgressDatabase,
IGoalProgressClient,
IKubrowPetPrintClient,
IKubrowPetPrintDatabase
} from "@/src/types/inventoryTypes/inventoryTypes";
@ -121,7 +121,7 @@ import {
export const typeCountSchema = new Schema<ITypeCount>({ ItemType: String, ItemCount: Number }, { _id: false });
typeCountSchema.set("toJSON", {
transform(_doc, obj) {
transform(_doc, obj: Record<string, any>) {
if (obj.ItemCount > 2147483647) {
obj.ItemCount = 2147483647;
} else if (obj.ItemCount < -2147483648) {
@ -189,7 +189,7 @@ operatorConfigSchema.virtual("ItemId").get(function () {
operatorConfigSchema.set("toJSON", {
virtuals: true,
transform(_document, returnedObject) {
transform(_document, returnedObject: Record<string, any>) {
delete returnedObject._id;
delete returnedObject.__v;
}
@ -226,7 +226,7 @@ const ItemConfigSchema = new Schema<IItemConfig>(
);
ItemConfigSchema.set("toJSON", {
transform(_document, returnedObject) {
transform(_document, returnedObject: Record<string, any>) {
delete returnedObject.__v;
}
});
@ -261,7 +261,7 @@ RawUpgrades.virtual("LastAdded").get(function () {
RawUpgrades.set("toJSON", {
virtuals: true,
transform(_document, returnedObject) {
transform(_document, returnedObject: Record<string, any>) {
delete returnedObject._id;
delete returnedObject.__v;
}
@ -282,7 +282,7 @@ upgradeSchema.virtual("ItemId").get(function () {
upgradeSchema.set("toJSON", {
virtuals: true,
transform(_document, returnedObject) {
transform(_document, returnedObject: Record<string, any>) {
delete returnedObject._id;
delete returnedObject.__v;
}
@ -325,7 +325,7 @@ const crewMemberSchema = new Schema<ICrewMemberDatabase>(
crewMemberSchema.set("toJSON", {
virtuals: true,
transform(_doc, obj) {
transform(_doc, obj: Record<string, any>) {
const db = obj as ICrewMemberDatabase;
const client = obj as ICrewMemberClient;
@ -353,7 +353,7 @@ const FlavourItemSchema = new Schema(
);
FlavourItemSchema.set("toJSON", {
transform(_document, returnedObject) {
transform(_document, returnedObject: Record<string, any>) {
delete returnedObject._id;
delete returnedObject.__v;
}
@ -367,7 +367,7 @@ FlavourItemSchema.set("toJSON", {
);
MailboxSchema.set("toJSON", {
transform(_document, returnedObject) {
transform(_document, returnedObject: Record<string, any>) {
const mailboxDatabase = returnedObject as HydratedDocument<IMailboxDatabase, { __v?: number }>;
delete mailboxDatabase.__v;
(returnedObject as IMailboxClient).LastInboxId = toOid(mailboxDatabase.LastInboxId);
@ -386,7 +386,7 @@ const DuviriInfoSchema = new Schema<IDuviriInfo>(
);
DuviriInfoSchema.set("toJSON", {
transform(_document, returnedObject) {
transform(_document, returnedObject: Record<string, any>) {
delete returnedObject.__v;
}
});
@ -416,7 +416,7 @@ const droneSchema = new Schema<IDroneDatabase>(
);
droneSchema.set("toJSON", {
virtuals: true,
transform(_document, obj) {
transform(_document, obj: Record<string, any>) {
const client = obj as IDroneClient;
const db = obj as IDroneDatabase;
@ -445,7 +445,7 @@ const discoveredMarkerSchema = new Schema<IDiscoveredMarker>(
{ _id: false }
);
const personalGoalProgressSchema = new Schema<IPersonalGoalProgressDatabase>(
const personalGoalProgressSchema = new Schema<IGoalProgressDatabase>(
{
Best: Number,
Count: Number,
@ -457,9 +457,9 @@ const personalGoalProgressSchema = new Schema<IPersonalGoalProgressDatabase>(
personalGoalProgressSchema.set("toJSON", {
virtuals: true,
transform(_doc, obj) {
const db = obj as IPersonalGoalProgressDatabase;
const client = obj as IPersonalGoalProgressClient;
transform(_doc, obj: Record<string, any>) {
const db = obj as IGoalProgressDatabase;
const client = obj as IGoalProgressClient;
client._id = toOid(db.goalId);
@ -502,7 +502,7 @@ StepSequencersSchema.virtual("ItemId").get(function () {
StepSequencersSchema.set("toJSON", {
virtuals: true,
transform(_document, returnedObject) {
transform(_document, returnedObject: Record<string, any>) {
delete returnedObject._id;
delete returnedObject.__v;
}
@ -516,7 +516,7 @@ const kubrowPetEggSchema = new Schema<IKubrowPetEggDatabase>(
);
kubrowPetEggSchema.set("toJSON", {
virtuals: true,
transform(_document, obj) {
transform(_document, obj: Record<string, any>) {
const client = obj as IKubrowPetEggClient;
const db = obj as IKubrowPetEggDatabase;
@ -586,7 +586,7 @@ personalTechProjectSchema.virtual("ItemId").get(function () {
personalTechProjectSchema.set("toJSON", {
virtuals: true,
transform(_doc, ret, _options) {
transform(_doc, ret: Record<string, any>) {
delete ret._id;
delete ret.__v;
@ -687,7 +687,7 @@ const questKeysSchema = new Schema<IQuestKeyDatabase>(
);
questKeysSchema.set("toJSON", {
transform(_doc, ret, _options) {
transform(_doc, ret: Record<string, any>) {
const questKeysDatabase = ret as IQuestKeyDatabase;
if (questKeysDatabase.CompletionDate) {
@ -709,7 +709,7 @@ const invasionProgressSchema = new Schema<IInvasionProgressDatabase>(
);
invasionProgressSchema.set("toJSON", {
transform(_doc, obj) {
transform(_doc, obj: Record<string, any>) {
const db = obj as IInvasionProgressDatabase;
const client = obj as IInvasionProgressClient;
@ -748,7 +748,7 @@ weaponSkinsSchema.virtual("ItemId").get(function () {
weaponSkinsSchema.set("toJSON", {
virtuals: true,
transform(_doc, ret, _options) {
transform(_doc, ret: Record<string, any>) {
delete ret._id;
delete ret.__v;
}
@ -772,7 +772,7 @@ const periodicMissionCompletionsSchema = new Schema<IPeriodicMissionCompletionDa
);
periodicMissionCompletionsSchema.set("toJSON", {
transform(_doc, ret, _options) {
transform(_doc, ret: Record<string, any>) {
const periodicMissionCompletionDatabase = ret as IPeriodicMissionCompletionDatabase;
(periodicMissionCompletionDatabase as unknown as IPeriodicMissionCompletionResponse).date = toMongoDate(
@ -849,7 +849,7 @@ const endlessXpProgressSchema = new Schema<IEndlessXpProgressDatabase>(
);
endlessXpProgressSchema.set("toJSON", {
transform(_doc, ret) {
transform(_doc, ret: Record<string, any>) {
const db = ret as IEndlessXpProgressDatabase;
const client = ret as IEndlessXpProgressClient;
@ -898,7 +898,7 @@ const crewShipMemberSchema = new Schema<ICrewShipMemberDatabase>(
);
crewShipMemberSchema.set("toJSON", {
virtuals: true,
transform(_doc, obj) {
transform(_doc, obj: Record<string, any>) {
const db = obj as ICrewShipMemberDatabase;
const client = obj as ICrewShipMemberClient;
if (db.ItemId) {
@ -951,7 +951,7 @@ const dialogueSchema = new Schema<IDialogueDatabase>(
);
dialogueSchema.set("toJSON", {
virtuals: true,
transform(_doc, ret) {
transform(_doc, ret: Record<string, any>) {
const db = ret as IDialogueDatabase;
const client = ret as IDialogueClient;
@ -997,7 +997,7 @@ const kubrowPetPrintSchema = new Schema<IKubrowPetPrintDatabase>({
});
kubrowPetPrintSchema.set("toJSON", {
virtuals: true,
transform(_doc, obj) {
transform(_doc, obj: Record<string, any>) {
const db = obj as IKubrowPetPrintDatabase;
const client = obj as IKubrowPetPrintClient;
@ -1025,7 +1025,7 @@ const detailsSchema = new Schema<IKubrowPetDetailsDatabase>(
);
detailsSchema.set("toJSON", {
transform(_doc, returnedObject) {
transform(_doc, returnedObject: Record<string, any>) {
delete returnedObject.__v;
const db = returnedObject as IKubrowPetDetailsDatabase;
@ -1081,7 +1081,7 @@ EquipmentSchema.virtual("ItemId").get(function () {
EquipmentSchema.set("toJSON", {
virtuals: true,
transform(_document, returnedObject) {
transform(_document, returnedObject: Record<string, any>) {
delete returnedObject._id;
delete returnedObject.__v;
@ -1132,7 +1132,7 @@ pendingRecipeSchema.virtual("ItemId").get(function () {
pendingRecipeSchema.set("toJSON", {
virtuals: true,
transform(_document, returnedObject) {
transform(_document, returnedObject: Record<string, any>) {
delete returnedObject._id;
delete returnedObject.__v;
delete returnedObject.LongGuns;
@ -1170,7 +1170,7 @@ const infestedFoundrySchema = new Schema<IInfestedFoundryDatabase>(
);
infestedFoundrySchema.set("toJSON", {
transform(_doc, ret, _options) {
transform(_doc, ret: Record<string, any>) {
if (ret.AbilityOverrideUnlockCooldown) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
ret.AbilityOverrideUnlockCooldown = toMongoDate(ret.AbilityOverrideUnlockCooldown);
@ -1216,8 +1216,8 @@ const calenderProgressSchema = new Schema<ICalendarProgress>(
},
SeasonProgress: {
SeasonType: { type: String, required: true },
LastCompletedDayIdx: { type: Number, default: 0 },
LastCompletedChallengeDayIdx: { type: Number, default: 0 },
LastCompletedDayIdx: { type: Number, default: -1 },
LastCompletedChallengeDayIdx: { type: Number, default: -1 },
ActivatedChallenges: { type: [String], default: [] }
}
},
@ -1243,7 +1243,7 @@ const vendorPurchaseHistoryEntrySchema = new Schema<IVendorPurchaseHistoryEntryD
);
vendorPurchaseHistoryEntrySchema.set("toJSON", {
transform(_doc, obj) {
transform(_doc, obj: Record<string, any>) {
const db = obj as IVendorPurchaseHistoryEntryDatabase;
const client = obj as IVendorPurchaseHistoryEntryClient;
client.Expiry = toMongoDate(db.Expiry);
@ -1286,7 +1286,7 @@ const pendingCouponSchema = new Schema<IPendingCouponDatabase>(
);
pendingCouponSchema.set("toJSON", {
transform(_doc, ret, _options) {
transform(_doc, ret: Record<string, any>) {
(ret as IPendingCouponClient).Expiry = toMongoDate((ret as IPendingCouponDatabase).Expiry);
}
});
@ -1353,7 +1353,7 @@ const nemesisSchema = new Schema<INemesisDatabase>(
nemesisSchema.set("toJSON", {
virtuals: true,
transform(_doc, obj) {
transform(_doc, obj: Record<string, any>) {
const db = obj as INemesisDatabase;
const client = obj as INemesisClient;
@ -1383,7 +1383,7 @@ const lastSortieRewardSchema = new Schema<ILastSortieRewardDatabase>(
lastSortieRewardSchema.set("toJSON", {
virtuals: true,
transform(_doc, obj) {
transform(_doc, obj: Record<string, any>) {
const db = obj as ILastSortieRewardDatabase;
const client = obj as ILastSortieRewardClient;
@ -1425,6 +1425,14 @@ const hubNpcCustomizationSchema = new Schema<IHubNpcCustomization>(
const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
{
accountOwnerId: Schema.Types.ObjectId,
// SNS account cheats
infiniteCredits: Boolean,
infinitePlatinum: Boolean,
infiniteEndo: Boolean,
infiniteRegalAya: Boolean,
infiniteHelminthMaterials: Boolean,
SubscribedToEmails: { type: Number, default: 0 },
SubscribedToEmailsPersonalized: { type: Number, default: 0 },
RewardSeed: BigInt,
@ -1437,6 +1445,8 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
PremiumCreditsFree: { type: Number, default: 0 },
//Endo
FusionPoints: { type: Number, default: 0 },
//Dirac
CrewShipFusionPoints: { type: Number, default: 0 },
//Regal Aya
PrimeTokens: { type: Number, default: 0 },
@ -1790,7 +1800,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
);
inventorySchema.set("toJSON", {
transform(_document, returnedObject) {
transform(_document, returnedObject: Record<string, any>) {
delete returnedObject._id;
delete returnedObject.__v;
delete returnedObject.accountOwnerId;

View File

@ -25,7 +25,7 @@ export const EquipmentSelectionSchema = new Schema<IEquipmentSelection>(
}
);
const loadoutConfigSchema = new Schema<ILoadoutConfigDatabase>(
export const loadoutConfigSchema = new Schema<ILoadoutConfigDatabase>(
{
FocusSchool: String,
PresetIcon: String,
@ -49,7 +49,7 @@ loadoutConfigSchema.virtual("ItemId").get(function () {
loadoutConfigSchema.set("toJSON", {
virtuals: true,
transform(_doc, ret, _options) {
transform(_doc, ret: Record<string, any>) {
delete ret._id;
delete ret.__v;
}
@ -62,6 +62,7 @@ export const loadoutSchema = new Schema<ILoadoutDatabase, loadoutModelType>({
NORMAL_PVP: [loadoutConfigSchema],
LUNARO: [loadoutConfigSchema],
OPERATOR: [loadoutConfigSchema],
GEAR: [loadoutConfigSchema],
KDRIVE: [loadoutConfigSchema],
DATAKNIFE: [loadoutConfigSchema],
MECH: [loadoutConfigSchema],
@ -71,7 +72,7 @@ export const loadoutSchema = new Schema<ILoadoutDatabase, loadoutModelType>({
});
loadoutSchema.set("toJSON", {
transform(_doc, ret, _options) {
transform(_doc, ret: Record<string, any>) {
delete ret._id;
delete ret.__v;
delete ret.loadoutOwnerId;
@ -88,6 +89,7 @@ type loadoutDocumentProps = {
NORMAL_PVP: Types.DocumentArray<ILoadoutConfigDatabase>;
LUNARO: Types.DocumentArray<ILoadoutConfigDatabase>;
OPERATOR: Types.DocumentArray<ILoadoutConfigDatabase>;
GEAR: Types.DocumentArray<ILoadoutConfigDatabase>;
KDRIVE: Types.DocumentArray<ILoadoutConfigDatabase>;
DATAKNIFE: Types.DocumentArray<ILoadoutConfigDatabase>;
MECH: Types.DocumentArray<ILoadoutConfigDatabase>;

View File

@ -32,7 +32,7 @@ const databaseAccountSchema = new Schema<IDatabaseAccountJson>(
);
databaseAccountSchema.set("toJSON", {
transform(_document, returnedObject) {
transform(_document, returnedObject: Record<string, any>) {
delete returnedObject._id;
delete returnedObject.__v;
},

View File

@ -1,6 +1,7 @@
import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers";
import {
IApartmentDatabase,
ICustomizationInfoDatabase,
IFavouriteLoadoutDatabase,
IGardeningDatabase,
IOrbiterClient,
@ -11,12 +12,13 @@ import {
IPlantClient,
IPlantDatabase,
IPlanterDatabase,
IRoom,
IRoomDatabase,
ITailorShopDatabase,
PersonalRoomsModelType
} from "@/src/types/personalRoomsTypes";
import { Schema, Types, model } from "mongoose";
import { colorSchema, shipCustomizationSchema } from "@/src/models/commonModel";
import { loadoutConfigSchema } from "@/src/models/inventoryModels/loadoutModel";
export const pictureFrameInfoSchema = new Schema<IPictureFrameInfo>(
{
@ -34,7 +36,20 @@ export const pictureFrameInfoSchema = new Schema<IPictureFrameInfo>(
TextColorB: Number,
TextOrientation: Number
},
{ id: false, _id: false }
{ _id: false }
);
export const customizationInfoSchema = new Schema<ICustomizationInfoDatabase>(
{
Anim: String,
AnimPose: Number,
LoadOutPreset: loadoutConfigSchema,
VehiclePreset: loadoutConfigSchema,
EquippedWeapon: String,
AvatarType: String,
LoadOutType: String
},
{ _id: false }
);
const placedDecosSchema = new Schema<IPlacedDecosDatabase>(
@ -44,7 +59,9 @@ const placedDecosSchema = new Schema<IPlacedDecosDatabase>(
Rot: [Number],
Scale: Number,
Sockets: Number,
PictureFrameInfo: { type: pictureFrameInfoSchema, default: undefined }
PictureFrameInfo: { type: pictureFrameInfoSchema, default: undefined },
CustomizationInfo: { type: customizationInfoSchema, default: undefined },
AnimPoseItem: String
},
{ id: false }
);
@ -55,12 +72,12 @@ placedDecosSchema.virtual("id").get(function (this: IPlacedDecosDatabase) {
placedDecosSchema.set("toJSON", {
virtuals: true,
transform(_document, returnedObject) {
transform(_document, returnedObject: Record<string, any>) {
delete returnedObject._id;
}
});
const roomSchema = new Schema<IRoom>(
const roomSchema = new Schema<IRoomDatabase>(
{
Name: String,
MaxCapacity: Number,
@ -78,7 +95,7 @@ const favouriteLoadoutSchema = new Schema<IFavouriteLoadoutDatabase>(
);
favouriteLoadoutSchema.set("toJSON", {
virtuals: true,
transform(_document, returnedObject) {
transform(_document, returnedObject: Record<string, any>) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
returnedObject.LoadoutId = toOid(returnedObject.LoadoutId);
}
@ -95,7 +112,7 @@ const plantSchema = new Schema<IPlantDatabase>(
plantSchema.set("toJSON", {
virtuals: true,
transform(_doc, obj) {
transform(_doc, obj: Record<string, any>) {
const client = obj as IPlantClient;
const db = obj as IPlantDatabase;
@ -122,7 +139,9 @@ const apartmentSchema = new Schema<IApartmentDatabase>(
{
Rooms: [roomSchema],
FavouriteLoadouts: [favouriteLoadoutSchema],
Gardening: gardeningSchema
Gardening: gardeningSchema,
VideoWallBackdrop: String,
Soundscape: String
},
{ _id: false }
);
@ -156,7 +175,7 @@ const orbiterSchema = new Schema<IOrbiterDatabase>(
);
orbiterSchema.set("toJSON", {
virtuals: true,
transform(_doc, obj) {
transform(_doc, obj: Record<string, any>) {
const db = obj as IOrbiterDatabase;
const client = obj as IOrbiterClient;

View File

@ -22,7 +22,7 @@ shipSchema.virtual("ItemId").get(function () {
shipSchema.set("toJSON", {
virtuals: true,
transform(_document, returnedObject) {
transform(_document, returnedObject: Record<string, any>) {
const shipResponse = returnedObject as IShipInventory;
const shipDatabase = returnedObject as IShipDatabase;
delete returnedObject._id;

View File

@ -101,7 +101,7 @@ const statsSchema = new Schema<IStatsDatabase>({
});
statsSchema.set("toJSON", {
transform(_document, returnedObject) {
transform(_document, returnedObject: Record<string, any>) {
delete returnedObject._id;
delete returnedObject.__v;
delete returnedObject.accountOwnerId;

View File

@ -10,6 +10,7 @@ import { addPendingFriendController } from "@/src/controllers/api/addPendingFrie
import { addToAllianceController } from "@/src/controllers/api/addToAllianceController";
import { addToGuildController } from "@/src/controllers/api/addToGuildController";
import { adoptPetController } from "@/src/controllers/api/adoptPetController";
import { apartmentController } from "@/src/controllers/api/apartmentController";
import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonController";
import { archonFusionController } from "@/src/controllers/api/archonFusionController";
import { artifactsController } from "@/src/controllers/api/artifactsController";
@ -61,10 +62,12 @@ import { getFriendsController } from "@/src/controllers/api/getFriendsController
import { getGuildContributionsController } from "@/src/controllers/api/getGuildContributionsController";
import { getGuildController } from "@/src/controllers/api/getGuildController";
import { getGuildDojoController } from "@/src/controllers/api/getGuildDojoController";
import { getGuildEventScoreController } from "@/src/controllers/api/getGuildEventScore";
import { getGuildLogController } from "@/src/controllers/api/getGuildLogController";
import { getIgnoredUsersController } from "@/src/controllers/api/getIgnoredUsersController";
import { getNewRewardSeedController } from "@/src/controllers/api/getNewRewardSeedController";
import { getProfileViewingDataPostController } from "@/src/controllers/dynamic/getProfileViewingDataController";
import { getPastWeeklyChallengesController } from "@/src/controllers/api/getPastWeeklyChallengesController";
import { getShipController } from "@/src/controllers/api/getShipController";
import { getVendorInfoController } from "@/src/controllers/api/getVendorInfoController";
import { getVoidProjectionRewardsController } from "@/src/controllers/api/getVoidProjectionRewardsController";
@ -112,6 +115,7 @@ import { removeFromGuildController } from "@/src/controllers/api/removeFromGuild
import { removeIgnoredUserController } from "@/src/controllers/api/removeIgnoredUserController";
import { renamePetController } from "@/src/controllers/api/renamePetController";
import { rerollRandomModController } from "@/src/controllers/api/rerollRandomModController";
import { resetQuestProgressController } from "@/src/controllers/api/resetQuestProgressController";
import { retrievePetFromStasisController } from "@/src/controllers/api/retrievePetFromStasisController";
import { saveDialogueController } from "@/src/controllers/api/saveDialogueController";
import { saveLoadoutController } from "@/src/controllers/api/saveLoadoutController";
@ -167,6 +171,7 @@ const apiRouter = express.Router();
// get
apiRouter.get("/abandonLibraryDailyTask.php", abandonLibraryDailyTaskController);
apiRouter.get("/abortDojoComponentDestruction.php", abortDojoComponentDestructionController);
apiRouter.get("/apartment.php", apartmentController);
apiRouter.get("/cancelGuildAdvertisement.php", cancelGuildAdvertisementController);
apiRouter.get("/changeDojoRoot.php", changeDojoRootController);
apiRouter.get("/changeGuildRank.php", changeGuildRankController);
@ -188,10 +193,12 @@ apiRouter.get("/getFriends.php", getFriendsController);
apiRouter.get("/getGuild.php", getGuildController);
apiRouter.get("/getGuildContributions.php", getGuildContributionsController);
apiRouter.get("/getGuildDojo.php", getGuildDojoController);
apiRouter.get("/getGuildEventScore.php", getGuildEventScoreController);
apiRouter.get("/getGuildLog.php", getGuildLogController);
apiRouter.get("/getIgnoredUsers.php", getIgnoredUsersController);
apiRouter.get("/getMessages.php", inboxController); // unsure if this is correct, but needed for U17
apiRouter.get("/getNewRewardSeed.php", getNewRewardSeedController);
apiRouter.get("/getPastWeeklyChallenges.php", getPastWeeklyChallengesController)
apiRouter.get("/getShip.php", getShipController);
apiRouter.get("/getShipDecos.php", (_req, res) => { res.end(); }); // needed to log in on U22.8
apiRouter.get("/getVendorInfo.php", getVendorInfoController);
@ -209,6 +216,7 @@ apiRouter.get("/questControl.php", questControlController);
apiRouter.get("/queueDojoComponentDestruction.php", queueDojoComponentDestructionController);
apiRouter.get("/removeFriend.php", removeFriendGetController);
apiRouter.get("/removeFromAlliance.php", removeFromAllianceController);
apiRouter.get("/resetQuestProgress.php", resetQuestProgressController);
apiRouter.get("/setActiveQuest.php", setActiveQuestController);
apiRouter.get("/setActiveShip.php", setActiveShipController);
apiRouter.get("/setAllianceGuildPermissions.php", setAllianceGuildPermissionsController);

View File

@ -15,6 +15,7 @@ import { webuiFileChangeDetectedController } from "@/src/controllers/custom/webu
import { completeAllMissionsController } from "@/src/controllers/custom/completeAllMissionsController";
import { addMissingHelminthBlueprintsController } from "@/src/controllers/custom/addMissingHelminthBlueprintsController";
import { abilityOverrideController } from "@/src/controllers/custom/abilityOverrideController";
import { createAccountController } from "@/src/controllers/custom/createAccountController";
import { createMessageController } from "@/src/controllers/custom/createMessageController";
import { addCurrencyController } from "@/src/controllers/custom/addCurrencyController";
@ -25,6 +26,9 @@ import { manageQuestsController } from "@/src/controllers/custom/manageQuestsCon
import { setEvolutionProgressController } from "@/src/controllers/custom/setEvolutionProgressController";
import { setBoosterController } from "@/src/controllers/custom/setBoosterController";
import { updateFingerprintController } from "@/src/controllers/custom/updateFingerprintController";
import { changeModularPartsController } from "@/src/controllers/custom/changeModularPartsController";
import { editSuitInvigorationUpgradeController } from "@/src/controllers/custom/editSuitInvigorationUpgradeController";
import { setAccountCheatController } from "@/src/controllers/custom/setAccountCheatController";
import { getConfigController, setConfigController } from "@/src/controllers/custom/configController";
@ -45,6 +49,7 @@ customRouter.get("/webuiFileChangeDetected", webuiFileChangeDetectedController);
customRouter.get("/completeAllMissions", completeAllMissionsController);
customRouter.get("/addMissingHelminthBlueprints", addMissingHelminthBlueprintsController);
customRouter.post("/abilityOverride", abilityOverrideController);
customRouter.post("/createAccount", createAccountController);
customRouter.post("/createMessage", createMessageController);
customRouter.post("/addCurrency", addCurrencyController);
@ -55,6 +60,9 @@ customRouter.post("/manageQuests", manageQuestsController);
customRouter.post("/setEvolutionProgress", setEvolutionProgressController);
customRouter.post("/setBooster", setBoosterController);
customRouter.post("/updateFingerprint", updateFingerprintController);
customRouter.post("/changeModularParts", changeModularPartsController);
customRouter.post("/editSuitInvigorationUpgrade", editSuitInvigorationUpgradeController);
customRouter.post("/setAccountCheat", setAccountCheatController);
customRouter.post("/getConfig", getConfigController);
customRouter.post("/setConfig", setConfigController);

View File

@ -2,6 +2,7 @@ import fs from "fs";
import path from "path";
import { repoDir } from "@/src/helpers/pathHelper";
import { args } from "@/src/helpers/commandLineArguments";
import { Inbox } from "@/src/models/inboxModel";
export interface IConfig {
mongodbUrl: string;
@ -19,11 +20,6 @@ export interface IConfig {
skipTutorial?: boolean;
skipAllDialogue?: boolean;
unlockAllScans?: boolean;
infiniteCredits?: boolean;
infinitePlatinum?: boolean;
infiniteEndo?: boolean;
infiniteRegalAya?: boolean;
infiniteHelminthMaterials?: boolean;
claimingBlueprintRefundsIngredients?: boolean;
dontSubtractPurchaseCreditCost?: boolean;
dontSubtractPurchasePlatinumCost?: boolean;
@ -80,8 +76,25 @@ export interface IConfig {
creditBoost?: boolean;
affinityBoost?: boolean;
resourceBoost?: boolean;
starDays?: boolean;
tennoLiveRelay?: boolean;
baroTennoConRelay?: boolean;
wolfHunt?: boolean;
longShadow?: boolean;
hallowedFlame?: boolean;
hallowedNightmares?: boolean;
hallowedNightmaresRewardsOverride?: number;
proxyRebellion?: boolean;
proxyRebellionRewardsOverride?: number;
galleonOfGhouls?: number;
ghoulEmergenceOverride?: boolean;
plagueStarOverride?: boolean;
starDaysOverride?: boolean;
dogDaysOverride?: boolean;
dogDaysRewardsOverride?: number;
bellyOfTheBeast?: boolean;
bellyOfTheBeastProgressOverride?: number;
eightClaw?: boolean;
eightClawProgressOverride?: number;
eidolonOverride?: string;
vallisOverride?: string;
duviriOverride?: string;
@ -113,9 +126,26 @@ export const loadConfig = (): void => {
// Set all values to undefined now so if the new config.json omits some fields that were previously present, it's correct in-memory.
for (const key of Object.keys(config)) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
(config as any)[key] = undefined;
}
Object.assign(config, newConfig);
};
export const syncConfigWithDatabase = (): void => {
// Event messages are deleted after endDate. Since we don't use beginDate/endDate and instead have config toggles, we need to delete the messages once those bools are false.
// Also, for some reason, I can't just do `Inbox.deleteMany(...)`; - it needs this whole circus.
if (!config.worldState?.creditBoost) {
void Inbox.deleteMany({ globaUpgradeId: "5b23106f283a555109666672" }).then(() => {});
}
if (!config.worldState?.affinityBoost) {
void Inbox.deleteMany({ globaUpgradeId: "5b23106f283a555109666673" }).then(() => {});
}
if (!config.worldState?.resourceBoost) {
void Inbox.deleteMany({ globaUpgradeId: "5b23106f283a555109666674" }).then(() => {});
}
if (!config.worldState?.galleonOfGhouls) {
void Inbox.deleteMany({ goalTag: "GalleonRobbery" }).then(() => {});
}
};

View File

@ -1,10 +1,9 @@
import chokidar from "chokidar";
import { logger } from "@/src/utils/logger";
import { config, configPath, loadConfig } from "@/src/services/configService";
import { config, configPath, loadConfig, syncConfigWithDatabase } from "@/src/services/configService";
import { saveConfig, shouldReloadConfig } from "@/src/services/configWriterService";
import { getWebPorts, startWebServer, stopWebServer } from "@/src/services/webService";
import { sendWsBroadcast } from "@/src/services/wsService";
import { Inbox } from "@/src/models/inboxModel";
import varzia from "@/static/fixed_responses/worldState/varzia.json";
chokidar.watch(configPath).on("change", () => {
@ -68,10 +67,3 @@ export const validateConfig = (): void => {
void saveConfig();
}
};
export const syncConfigWithDatabase = (): void => {
// Event messages are deleted after endDate. Since we don't use beginDate/endDate and instead have config toggles, we need to delete the messages once those bools are false.
if (!config.worldState?.galleonOfGhouls) {
void Inbox.deleteMany({ goalTag: "GalleonRobbery" }).then(() => {}); // For some reason, I can't just do `Inbox.deleteMany(...)`; it needs this whole circus.
}
};

View File

@ -13,6 +13,7 @@ import {
IDojoComponentDatabase,
IDojoContributable,
IDojoDecoClient,
IDojoDecoDatabase,
IGuildClient,
IGuildMemberClient,
IGuildMemberDatabase,
@ -114,7 +115,14 @@ export const getGuildClient = async (
NumContributors: guild.CeremonyContributors?.length ?? 0,
CeremonyResetDate: guild.CeremonyResetDate ? toMongoDate(guild.CeremonyResetDate) : undefined,
AutoContributeFromVault: guild.AutoContributeFromVault,
AllianceId: guild.AllianceId ? toOid2(guild.AllianceId, account.BuildLabel) : undefined
AllianceId: guild.AllianceId ? toOid2(guild.AllianceId, account.BuildLabel) : undefined,
GoalProgress: guild.GoalProgress
? guild.GoalProgress.map(gp => ({
Count: gp.Count,
Tag: gp.Tag,
_id: { $oid: gp.goalId.toString() }
}))
: undefined
};
};
@ -309,7 +317,7 @@ export const removeDojoRoom = async (
guild.DojoEnergy -= meta.energy;
}
moveResourcesToVault(guild, component);
component.Decos?.forEach(deco => moveResourcesToVault(guild, deco));
component.Decos?.forEach(deco => refundDojoDeco(guild, component, deco));
if (guild.RoomChanges) {
const index = guild.RoomChanges.findIndex(x => x.componentId.equals(component._id));
@ -344,6 +352,14 @@ export const removeDojoDeco = (
component.Decos!.findIndex(x => x._id.equals(decoId)),
1
)[0];
refundDojoDeco(guild, component, deco);
};
export const refundDojoDeco = (
guild: TGuildDatabaseDocument,
component: IDojoComponentDatabase,
deco: IDojoDecoDatabase
): void => {
const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == deco.Type);
if (meta) {
if (meta.capacityCost) {
@ -369,7 +385,7 @@ export const removeDojoDeco = (
]);
}
}
moveResourcesToVault(guild, deco);
moveResourcesToVault(guild, deco); // Refund resources spent on construction
};
const moveResourcesToVault = (guild: TGuildDatabaseDocument, component: IDojoContributable): void => {
@ -800,3 +816,85 @@ export const getAllianceClient = async (
}
};
};
export const handleGuildGoalProgress = async (
guild: TGuildDatabaseDocument,
upload: { Count: number; Tag: string; goalId: Types.ObjectId }
): Promise<void> => {
guild.GoalProgress ??= [];
const goalProgress = guild.GoalProgress.find(x => x.goalId.equals(upload.goalId));
if (!goalProgress) {
guild.GoalProgress.push({
Count: upload.Count,
Tag: upload.Tag,
goalId: upload.goalId
});
}
const totalCount = (goalProgress?.Count ?? 0) + upload.Count;
const guildRewards = goalGuildRewardByTag[upload.Tag].rewards;
const tierGoals = goalGuildRewardByTag[upload.Tag].guildGoals[guild.Tier - 1];
const rewards = [];
if (tierGoals.length && guildRewards.length) {
for (let i = 0; i < tierGoals.length; i++) {
if (
tierGoals[i] &&
tierGoals[i] <= totalCount &&
(!goalProgress || goalProgress.Count < tierGoals[i]) &&
guildRewards[i]
) {
rewards.push(guildRewards[i]);
}
}
if (rewards.length) {
logger.debug(`guild goal rewards`, rewards);
guild.VaultDecoRecipes ??= [];
rewards.forEach(type => {
guild.VaultDecoRecipes!.push({
ItemType: type,
ItemCount: 1
});
});
}
}
if (goalProgress) {
goalProgress.Count += upload.Count;
}
await guild.save();
};
export const goalGuildRewardByTag: Record<string, { guildGoals: number[][]; rewards: string[] }> = {
JadeShadowsEvent: {
guildGoals: [
// I don't know what ClanGoal means
[15, 30, 45, 60],
[45, 90, 135, 180],
[150, 300, 450, 600],
[450, 900, 1350, 1800],
[1500, 3000, 4500, 6000]
],
rewards: [
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/JadeShadowsEventPewterTrophyRecipe",
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/JadeShadowsEventBronzeTrophyRecipe",
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/JadeShadowsEventSilverTrophyRecipe",
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/JadeShadowsEventGoldTrophyRecipe"
]
},
DuviriMurmurEvent: {
guildGoals: [
// I don't know what ClanGoal means
[260, 519, 779, 1038],
[779, 1557, 2336, 3114],
[2595, 5190, 7785, 10380],
[7785, 15570, 23355, 31140],
[29950, 51900, 77850, 103800]
],
rewards: [
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/DuviriMurmurEventClayTrophyRecipe",
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/DuviriMurmurEventBronzeTrophyRecipe",
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/DuviriMurmurEventSilverTrophyRecipe",
"/Lotus/Levels/ClanDojo/ComponentPropRecipes/DuviriMurmurEventGoldTrophyRecipe"
]
}
};

View File

@ -44,6 +44,28 @@ import {
IKubrowPetDetailsClient,
IKubrowPetDetailsDatabase
} from "@/src/types/equipmentTypes";
import {
IApartmentClient,
IApartmentDatabase,
ICustomizationInfoClient,
ICustomizationInfoDatabase,
IFavouriteLoadout,
IFavouriteLoadoutDatabase,
IGetShipResponse,
IOrbiterClient,
IOrbiterDatabase,
IPersonalRoomsDatabase,
IPlacedDecosClient,
IPlacedDecosDatabase,
IPlantClient,
IPlantDatabase,
IPlanterClient,
IPlanterDatabase,
IRoomClient,
IRoomDatabase,
ITailorShop,
ITailorShopDatabase
} from "@/src/types/personalRoomsTypes";
const convertDate = (value: IMongoDate): Date => {
return new Date(parseInt(value.$date.$numberLong));
@ -422,9 +444,91 @@ export const importLoadOutPresets = (db: ILoadoutDatabase, client: ILoadOutPrese
db.NORMAL_PVP = client.NORMAL_PVP.map(convertLoadOutConfig);
db.LUNARO = client.LUNARO.map(convertLoadOutConfig);
db.OPERATOR = client.OPERATOR.map(convertLoadOutConfig);
db.GEAR = client.GEAR.map(convertLoadOutConfig);
db.KDRIVE = client.KDRIVE.map(convertLoadOutConfig);
db.DATAKNIFE = client.DATAKNIFE.map(convertLoadOutConfig);
db.MECH = client.MECH.map(convertLoadOutConfig);
db.OPERATOR_ADULT = client.OPERATOR_ADULT.map(convertLoadOutConfig);
db.DRIFTER = client.DRIFTER.map(convertLoadOutConfig);
};
export const convertCustomizationInfo = (client: ICustomizationInfoClient): ICustomizationInfoDatabase => {
return {
...client,
LoadOutPreset: client.LoadOutPreset ? convertLoadOutConfig(client.LoadOutPreset) : undefined,
VehiclePreset: client.VehiclePreset ? convertLoadOutConfig(client.VehiclePreset) : undefined
};
};
const convertDeco = (client: IPlacedDecosClient): IPlacedDecosDatabase => {
const { id, ...rest } = client;
return {
...rest,
CustomizationInfo: client.CustomizationInfo ? convertCustomizationInfo(client.CustomizationInfo) : undefined,
_id: new Types.ObjectId(id.$oid)
};
};
const convertRoom = (client: IRoomClient): IRoomDatabase => {
return {
...client,
PlacedDecos: client.PlacedDecos ? client.PlacedDecos.map(convertDeco) : []
};
};
const convertShip = (client: IOrbiterClient): IOrbiterDatabase => {
return {
...client,
ShipInterior: {
...client.ShipInterior,
Colors: Array.isArray(client.ShipInterior.Colors) ? {} : client.ShipInterior.Colors
},
Rooms: client.Rooms.map(convertRoom),
FavouriteLoadoutId: client.FavouriteLoadoutId ? new Types.ObjectId(client.FavouriteLoadoutId.$oid) : undefined
};
};
const convertPlant = (client: IPlantClient): IPlantDatabase => {
return {
...client,
EndTime: convertDate(client.EndTime)
};
};
const convertPlanter = (client: IPlanterClient): IPlanterDatabase => {
return {
...client,
Plants: client.Plants.map(convertPlant)
};
};
const convertFavouriteLoadout = (client: IFavouriteLoadout): IFavouriteLoadoutDatabase => {
return {
...client,
LoadoutId: new Types.ObjectId(client.LoadoutId.$oid)
};
};
const convertApartment = (client: IApartmentClient): IApartmentDatabase => {
return {
...client,
Rooms: client.Rooms.map(convertRoom),
Gardening: { Planters: client.Gardening.Planters.map(convertPlanter) },
FavouriteLoadouts: client.FavouriteLoadouts ? client.FavouriteLoadouts.map(convertFavouriteLoadout) : []
};
};
const convertTailorShop = (client: ITailorShop): ITailorShopDatabase => {
return {
...client,
Rooms: client.Rooms.map(convertRoom),
Colors: Array.isArray(client.Colors) ? {} : client.Colors,
FavouriteLoadouts: client.FavouriteLoadouts ? client.FavouriteLoadouts.map(convertFavouriteLoadout) : []
};
};
export const importPersonalRooms = (db: IPersonalRoomsDatabase, client: Partial<IGetShipResponse>): void => {
if (client.Ship !== undefined) db.Ship = convertShip(client.Ship);
if (client.Apartment !== undefined) db.Apartment = convertApartment(client.Apartment);
if (client.TailorShop !== undefined) db.TailorShop = convertTailorShop(client.TailorShop);
};

View File

@ -35,7 +35,7 @@ export const createNewEventMessages = async (req: Request): Promise<void> => {
const baroIndex = Math.trunc((Date.now() - 910800000) / (unixTimesInMs.day * 14));
const baroStart = baroIndex * (unixTimesInMs.day * 14) + 910800000;
const baroActualStart = baroStart + unixTimesInMs.day * (config.baroAlwaysAvailable ? 0 : 12);
if (account.LatestEventMessageDate.getTime() < baroActualStart) {
if (Date.now() >= baroActualStart && account.LatestEventMessageDate.getTime() < baroActualStart) {
newEventMessages.push({
sndr: "/Lotus/Language/G1Quests/VoidTraderName",
sub: "/Lotus/Language/CommunityMessages/VoidTraderAppearanceTitle",
@ -55,20 +55,77 @@ export const createNewEventMessages = async (req: Request): Promise<void> => {
}
// BUG: Deleting the inbox message manually means it'll just be automatically re-created. This is because we don't use startDate/endDate for these config-toggled events.
if (config.worldState?.galleonOfGhouls) {
if (!(await Inbox.exists({ ownerId: account._id, goalTag: "GalleonRobbery" }))) {
newEventMessages.push({
sndr: "/Lotus/Language/Bosses/BossCouncilorVayHek",
sub: "/Lotus/Language/Events/GalleonRobberyIntroMsgTitle",
msg: "/Lotus/Language/Events/GalleonRobberyIntroMsgDesc",
icon: "/Lotus/Interface/Icons/Npcs/VayHekPortrait.png",
transmission: "/Lotus/Sounds/Dialog/GalleonOfGhouls/DGhoulsWeekOneInbox0010VayHek",
att: ["/Lotus/Upgrades/Skins/Events/OgrisOldSchool"],
startDate: new Date(),
goalTag: "GalleonRobbery"
});
}
const promises = [];
if (config.worldState?.creditBoost) {
promises.push(
(async (): Promise<void> => {
if (!(await Inbox.exists({ ownerId: account._id, globaUpgradeId: "5b23106f283a555109666672" }))) {
newEventMessages.push({
globaUpgradeId: new Types.ObjectId("5b23106f283a555109666672"),
sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
sub: "/Lotus/Language/Items/EventDoubleCreditsName",
msg: "/Lotus/Language/Items/EventDoubleCreditsDesc",
icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
startDate: new Date(),
CrossPlatform: true
});
}
})()
);
}
if (config.worldState?.affinityBoost) {
promises.push(
(async (): Promise<void> => {
if (!(await Inbox.exists({ ownerId: account._id, globaUpgradeId: "5b23106f283a555109666673" }))) {
newEventMessages.push({
globaUpgradeId: new Types.ObjectId("5b23106f283a555109666673"),
sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
sub: "/Lotus/Language/Items/EventDoubleAffinityName",
msg: "/Lotus/Language/Items/EventDoubleAffinityDesc",
icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
startDate: new Date(),
CrossPlatform: true
});
}
})()
);
}
if (config.worldState?.resourceBoost) {
promises.push(
(async (): Promise<void> => {
if (!(await Inbox.exists({ ownerId: account._id, globaUpgradeId: "5b23106f283a555109666674" }))) {
newEventMessages.push({
globaUpgradeId: new Types.ObjectId("5b23106f283a555109666674"),
sndr: "/Lotus/Language/Menu/Mailbox_WarframeSender",
sub: "/Lotus/Language/Items/EventDoubleResourceName",
msg: "/Lotus/Language/Items/EventDoubleResourceDesc",
icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png",
startDate: new Date(),
CrossPlatform: true
});
}
})()
);
}
if (config.worldState?.galleonOfGhouls) {
promises.push(
(async (): Promise<void> => {
if (!(await Inbox.exists({ ownerId: account._id, goalTag: "GalleonRobbery" }))) {
newEventMessages.push({
sndr: "/Lotus/Language/Bosses/BossCouncilorVayHek",
sub: "/Lotus/Language/Events/GalleonRobberyIntroMsgTitle",
msg: "/Lotus/Language/Events/GalleonRobberyIntroMsgDesc",
icon: "/Lotus/Interface/Icons/Npcs/VayHekPortrait.png",
transmission: "/Lotus/Sounds/Dialog/GalleonOfGhouls/DGhoulsWeekOneInbox0010VayHek",
att: ["/Lotus/Upgrades/Skins/Events/OgrisOldSchool"],
startDate: new Date(),
goalTag: "GalleonRobbery"
});
}
})()
);
}
await Promise.all(promises);
if (newEventMessages.length === 0) {
return;

View File

@ -1,8 +1,11 @@
import { ExportRecipes } from "warframe-public-export-plus";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { IInfestedFoundryClient, IInfestedFoundryDatabase } from "@/src/types/inventoryTypes/inventoryTypes";
import {
IAccountCheats,
IInfestedFoundryClient,
IInfestedFoundryDatabase
} from "@/src/types/inventoryTypes/inventoryTypes";
import { addRecipes } from "@/src/services/inventoryService";
import { config } from "@/src/services/configService";
import { ITypeCount } from "@/src/types/commonTypes";
export const addInfestedFoundryXP = (infestedFoundry: IInfestedFoundryDatabase, delta: number): ITypeCount[] => {
@ -97,8 +100,8 @@ export const handleSubsumeCompletion = (inventory: TInventoryDatabaseDocument):
return recipeChanges;
};
export const applyCheatsToInfestedFoundry = (infestedFoundry: IInfestedFoundryClient): void => {
if (config.infiniteHelminthMaterials) {
export const applyCheatsToInfestedFoundry = (cheats: IAccountCheats, infestedFoundry: IInfestedFoundryClient): void => {
if (cheats.infiniteHelminthMaterials) {
infestedFoundry.Resources = [
{ ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthCalx", Count: 1000 },
{ ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthBiotics", Count: 1000 },

View File

@ -25,7 +25,8 @@ import {
INemesisWeaponTargetFingerprint,
INemesisPetTargetFingerprint,
IDialogueDatabase,
IKubrowPetPrintClient
IKubrowPetPrintClient,
equipmentKeys
} from "@/src/types/inventoryTypes/inventoryTypes";
import { IGenericUpdate, IUpdateNodeIntrosResponse } from "@/src/types/genericUpdate";
import { IKeyChainRequest, IMissionInventoryUpdateRequest } from "@/src/types/requestTypes";
@ -67,7 +68,8 @@ import {
kubrowDetails,
kubrowFurPatternsWeights,
kubrowWeights,
toOid
toOid,
TTraitsPool
} from "@/src/helpers/inventoryHelpers";
import { addQuestKey, completeQuest } from "@/src/services/questService";
import { handleBundleAcqusition } from "@/src/services/purchaseService";
@ -481,11 +483,14 @@ export const addItem = async (
if (quantity != 1) {
logger.warn(`adding 1 of ${typeName} ${targetFingerprint} even tho quantity ${quantity} was requested`);
}
inventory.Upgrades.push({
ItemType: typeName,
UpgradeFingerprint: targetFingerprint
});
return {}; // there's not exactly a common "InventoryChanges" format for these
const upgrade =
inventory.Upgrades[
inventory.Upgrades.push({
ItemType: typeName,
UpgradeFingerprint: targetFingerprint
}) - 1
];
return { Upgrades: [upgrade.toJSON<IUpgradeClient>()] };
}
const changes = [
{
@ -811,7 +816,7 @@ export const addItem = async (
if (!seed) {
throw new Error(`Expected crew member to have a seed`);
}
seed |= 0x33b81en << 32n;
seed |= BigInt(Math.trunc(inventory.Created.getTime() / 1000) & 0xffffff) << 32n;
return {
...addCrewMember(inventory, typeName, seed),
...occupySlot(inventory, InventorySlot.CREWMEMBERS, premiumPurchase)
@ -842,6 +847,32 @@ export const addItem = async (
return addMotorcycle(inventory, typeName);
}
break;
case "Lore":
if (typeName == "/Lotus/Types/Lore/Fragments/GrineerGhoulFragments/GhoulFragmentRewards") {
const fragmentType = getRandomElement([
"/Lotus/Types/Lore/Fragments/GrineerGhoulFragments/GhoulFragmentA",
"/Lotus/Types/Lore/Fragments/GrineerGhoulFragments/GhoulFragmentB",
"/Lotus/Types/Lore/Fragments/GrineerGhoulFragments/GhoulFragmentC",
"/Lotus/Types/Lore/Fragments/GrineerGhoulFragments/GhoulFragmentD",
"/Lotus/Types/Lore/Fragments/GrineerGhoulFragments/GhoulFragmentE",
"/Lotus/Types/Lore/Fragments/GrineerGhoulFragments/GhoulFragmentF",
"/Lotus/Types/Lore/Fragments/GrineerGhoulFragments/GhoulFragmentG",
"/Lotus/Types/Lore/Fragments/GrineerGhoulFragments/GhoulFragmentH",
"/Lotus/Types/Lore/Fragments/GrineerGhoulFragments/GhoulFragmentI",
"/Lotus/Types/Lore/Fragments/GrineerGhoulFragments/GhoulFragmentJ",
"/Lotus/Types/Lore/Fragments/GrineerGhoulFragments/GhoulFragmentK",
"/Lotus/Types/Lore/Fragments/GrineerGhoulFragments/GhoulFragmentL",
"/Lotus/Types/Lore/Fragments/GrineerGhoulFragments/GhoulFragmentM"
])!;
addLoreFragmentScans(inventory, [
{
Progress: 1,
Region: "",
ItemType: fragmentType
}
]);
}
break;
}
break;
}
@ -1048,6 +1079,21 @@ export const addSpaceSuit = (
return inventoryChanges;
};
const createRandomTraits = (kubrowPetName: string, traitsPool: TTraitsPool): ITraits => {
return {
BaseColor: getRandomWeightedReward(traitsPool.Colors, kubrowWeights)!.type,
SecondaryColor: getRandomWeightedReward(traitsPool.Colors, kubrowWeights)!.type,
TertiaryColor: getRandomWeightedReward(traitsPool.Colors, kubrowWeights)!.type,
AccentColor: getRandomWeightedReward(traitsPool.Colors, kubrowWeights)!.type,
EyeColor: getRandomWeightedReward(traitsPool.EyeColors, kubrowWeights)!.type,
FurPattern: getRandomWeightedReward(traitsPool.FurPatterns, kubrowFurPatternsWeights)!.type,
Personality: kubrowPetName,
BodyType: getRandomWeightedReward(traitsPool.BodyTypes, kubrowWeights)!.type,
Head: traitsPool.Heads.length ? getRandomWeightedReward(traitsPool.Heads, kubrowWeights)!.type : undefined,
Tail: traitsPool.Tails.length ? getRandomWeightedReward(traitsPool.Tails, kubrowWeights)!.type : undefined
};
};
export const addKubrowPet = (
inventory: TInventoryDatabaseDocument,
kubrowPetName: string,
@ -1064,7 +1110,6 @@ export const addKubrowPet = (
addSpecialItem(inventory, specialItem, inventoryChanges);
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const configs: IItemConfig[] = applyDefaultUpgrades(inventory, kubrowPet?.defaultUpgrades);
if (!details) {
@ -1074,9 +1119,10 @@ export const addKubrowPet = (
"/Lotus/Types/Game/CatbrowPet/VampireCatbrowPetPowerSuit"
].includes(kubrowPetName);
let traits: ITraits;
const traitsPool = isCatbrow ? catbrowDetails : kubrowDetails;
let dominantTraits: ITraits;
if (kubrowPetName == "/Lotus/Types/Game/CatbrowPet/VampireCatbrowPetPowerSuit") {
traits = {
dominantTraits = {
BaseColor: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseVampire",
SecondaryColor: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryVampire",
TertiaryColor: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryVampire",
@ -1089,19 +1135,35 @@ export const addKubrowPet = (
Tail: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailVampire"
};
} else {
const traitsPool = isCatbrow ? catbrowDetails : kubrowDetails;
traits = {
BaseColor: getRandomWeightedReward(traitsPool.Colors, kubrowWeights)!.type,
SecondaryColor: getRandomWeightedReward(traitsPool.Colors, kubrowWeights)!.type,
TertiaryColor: getRandomWeightedReward(traitsPool.Colors, kubrowWeights)!.type,
AccentColor: getRandomWeightedReward(traitsPool.Colors, kubrowWeights)!.type,
EyeColor: getRandomWeightedReward(traitsPool.EyeColors, kubrowWeights)!.type,
FurPattern: getRandomWeightedReward(traitsPool.FurPatterns, kubrowFurPatternsWeights)!.type,
Personality: kubrowPetName,
BodyType: getRandomWeightedReward(traitsPool.BodyTypes, kubrowWeights)!.type,
Head: isCatbrow ? getRandomWeightedReward(traitsPool.Heads, kubrowWeights)!.type : undefined,
Tail: isCatbrow ? getRandomWeightedReward(traitsPool.Tails, kubrowWeights)!.type : undefined
};
dominantTraits = createRandomTraits(kubrowPetName, traitsPool);
if (kubrowPetName == "/Lotus/Types/Game/KubrowPet/ChargerKubrowPetPowerSuit") {
dominantTraits.BodyType = "/Lotus/Types/Game/KubrowPet/BodyTypes/ChargerKubrowPetBodyType";
dominantTraits.FurPattern = "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternInfested";
}
}
const recessiveTraits: ITraits = createRandomTraits(
getRandomElement(
isCatbrow
? [
"/Lotus/Types/Game/CatbrowPet/MirrorCatbrowPetPowerSuit",
"/Lotus/Types/Game/CatbrowPet/CheshireCatbrowPetPowerSuit"
]
: [
"/Lotus/Types/Game/KubrowPet/AdventurerKubrowPetPowerSuit",
"/Lotus/Types/Game/KubrowPet/FurtiveKubrowPetPowerSuit",
"/Lotus/Types/Game/KubrowPet/GuardKubrowPetPowerSuit",
"/Lotus/Types/Game/KubrowPet/HunterKubrowPetPowerSuit",
"/Lotus/Types/Game/KubrowPet/RetrieverKubrowPetPowerSuit"
]
)!,
traitsPool
);
for (const key of Object.keys(recessiveTraits) as (keyof ITraits)[]) {
// My heurstic approximation is a 20% chance for a dominant trait to be copied into the recessive traits. TODO: A more scientific statistical analysis maybe?
if (Math.random() < 0.2) {
recessiveTraits[key] = dominantTraits[key]!;
}
}
details = {
@ -1113,8 +1175,8 @@ export const addKubrowPet = (
HatchDate: premiumPurchase ? new Date() : new Date(Date.now() + 10 * unixTimesInMs.hour), // On live, this seems to be somewhat randomised so that the pet hatches 9~11 hours after start.
IsMale: !!getRandomInt(0, 1),
Size: getRandomInt(70, 100) / 100,
DominantTraits: traits,
RecessiveTraits: traits
DominantTraits: dominantTraits,
RecessiveTraits: recessiveTraits
};
}
@ -1165,8 +1227,8 @@ export const updateSlots = (
}
};
const isCurrencyTracked = (usePremium: boolean): boolean => {
return usePremium ? !config.infinitePlatinum : !config.infiniteCredits;
const isCurrencyTracked = (inventory: TInventoryDatabaseDocument, usePremium: boolean): boolean => {
return usePremium ? !inventory.infinitePlatinum : !inventory.infiniteCredits;
};
export const updateCurrency = (
@ -1175,7 +1237,7 @@ export const updateCurrency = (
usePremium: boolean,
inventoryChanges: IInventoryChanges = {}
): IInventoryChanges => {
if (price != 0 && isCurrencyTracked(usePremium)) {
if (price != 0 && isCurrencyTracked(inventory, usePremium)) {
if (usePremium) {
if (inventory.PremiumCreditsFree > 0) {
const premiumCreditsFreeDelta = Math.min(price, inventory.PremiumCreditsFree) * -1;
@ -1206,6 +1268,15 @@ export const addFusionPoints = (inventory: TInventoryDatabaseDocument, add: numb
return add;
};
export const addCrewShipFusionPoints = (inventory: TInventoryDatabaseDocument, add: number): number => {
if (inventory.CrewShipFusionPoints + add > 2147483647) {
logger.warn(`capping CrewShipFusionPoints balance at 2147483647`);
add = 2147483647 - inventory.CrewShipFusionPoints;
}
inventory.CrewShipFusionPoints += add;
return add;
};
const standingLimitBinToInventoryKey: Record<
Exclude<TStandingLimitBin, "STANDING_LIMIT_BIN_NONE">,
keyof IDailyAffiliations
@ -1297,7 +1368,7 @@ export const addStanding = (
// TODO: AffiliationMods support (Nightwave).
export const updateGeneric = async (data: IGenericUpdate, accountId: string): Promise<IUpdateNodeIntrosResponse> => {
const inventory = await getInventory(accountId, "NodeIntrosCompleted MiscItems");
const inventory = await getInventory(accountId, "NodeIntrosCompleted MiscItems ShipDecorations");
// Make it an array for easier parsing.
if (typeof data.NodeIntrosCompleted === "string") {
@ -1306,7 +1377,15 @@ export const updateGeneric = async (data: IGenericUpdate, accountId: string): Pr
const inventoryChanges: IInventoryChanges = {};
for (const node of data.NodeIntrosCompleted) {
if (node == "KayaFirstVisitPack") {
if (node == "TC2025") {
inventoryChanges.ShipDecorations = [
{
ItemType: "/Lotus/Types/Items/ShipDecos/TauGrineerLancerBobbleHead",
ItemCount: 1
}
];
addShipDecorations(inventory, inventoryChanges.ShipDecorations);
} else if (node == "KayaFirstVisitPack") {
inventoryChanges.MiscItems = [
{
ItemType: "/Lotus/Types/Items/MiscItems/1999FixedStickersPack",
@ -1600,6 +1679,15 @@ export const addEmailItem = async (
return inventoryChanges;
};
const xpEarningParts: readonly string[] = [
"LWPT_BLADE",
"LWPT_GUN_BARREL",
"LWPT_AMP_OCULUS",
"LWPT_MOA_HEAD",
"LWPT_ZANUKA_HEAD",
"LWPT_HB_DECK"
];
export const applyClientEquipmentUpdates = (
inventory: TInventoryDatabaseDocument,
gearArray: IEquipmentClient[],
@ -1618,15 +1706,34 @@ export const applyClientEquipmentUpdates = (
item.XP ??= 0;
item.XP += XP;
const xpinfoIndex = inventory.XPInfo.findIndex(x => x.ItemType == item.ItemType);
if (xpinfoIndex !== -1) {
const xpinfo = inventory.XPInfo[xpinfoIndex];
xpinfo.XP += XP;
} else {
inventory.XPInfo.push({
ItemType: item.ItemType,
XP: XP
});
if (
categoryName != "SpecialItems" ||
item.ItemType == "/Lotus/Powersuits/Khora/Kavat/KhoraKavatPowerSuit" ||
item.ItemType == "/Lotus/Powersuits/Khora/Kavat/KhoraPrimeKavatPowerSuit"
) {
let xpItemType = item.ItemType;
if (item.ModularParts) {
for (const part of item.ModularParts) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const partType = ExportWeapons[part]?.partType;
if (partType !== undefined && xpEarningParts.indexOf(partType) != -1) {
xpItemType = part;
break;
}
}
logger.debug(`adding xp to ${xpItemType} for modular item ${fromOid(ItemId)} (${item.ItemType})`);
}
const xpinfoIndex = inventory.XPInfo.findIndex(x => x.ItemType == xpItemType);
if (xpinfoIndex !== -1) {
const xpinfo = inventory.XPInfo[xpinfoIndex];
xpinfo.XP += XP;
} else {
inventory.XPInfo.push({
ItemType: xpItemType,
XP: XP
});
}
}
}
@ -1831,25 +1938,90 @@ export const addLoreFragmentScans = (inventory: TInventoryDatabaseDocument, arr:
});
};
export const addChallenges = (
const challengeRewardsInboxMessages: Record<string, IMessageCreationTemplate> = {
SentEvoEphemeraRankOne: {
sub: "/Lotus/Language/Inbox/EvolvingEphemeraUnlockAName",
sndr: "/Lotus/Language/Bosses/Ordis",
msg: "/Lotus/Language/Inbox/EvolvingEphemeraUnlockADesc",
icon: "/Lotus/Interface/Icons/Npcs/Ordis.png",
att: ["/Lotus/Upgrades/Skins/Effects/NarmerEvolvingEphemeraB"]
},
SentEvoEphemeraRankTwo: {
sub: "/Lotus/Language/Inbox/EvolvingEphemeraUnlockBName",
sndr: "/Lotus/Language/Bosses/Ordis",
msg: "/Lotus/Language/Inbox/EvolvingEphemeraUnlockBDesc",
icon: "/Lotus/Interface/Icons/Npcs/Ordis.png",
att: ["/Lotus/Upgrades/Skins/Effects/NarmerEvolvingEphemeraC"]
},
SentEvoSyandanaRankOne: {
sub: "/Lotus/Language/Inbox/EvolvingSyandanaUnlockAName",
sndr: "/Lotus/Language/Bosses/Ordis",
msg: "/Lotus/Language/Inbox/EvolvingSyandanaUnlockADesc",
icon: "/Lotus/Interface/Icons/Npcs/Ordis.png",
att: ["/Lotus/Upgrades/Skins/Scarves/NarmerEvolvingSyandanaBCape"]
},
SentEvoSyandanaRankTwo: {
sub: "/Lotus/Language/Inbox/EvolvingSyandanaUnlockBName",
sndr: "/Lotus/Language/Bosses/Ordis",
msg: "/Lotus/Language/Inbox/EvolvingSyandanaUnlockBDesc",
icon: "/Lotus/Interface/Icons/Npcs/Ordis.png",
att: ["/Lotus/Upgrades/Skins/Scarves/NarmerEvolvingSyandanaCCape"]
},
SentEvoSekharaRankOne: {
sub: "/Lotus/Language/Inbox/EvolvingSekharaUnlockAName",
sndr: "/Lotus/Language/Bosses/Ordis",
msg: "/Lotus/Language/Inbox/EvolvingSekharaUnlockADesc",
icon: "/Lotus/Interface/Icons/Npcs/Ordis.png",
att: ["/Lotus/Upgrades/Skins/Clan/ZarimanEvolvingSekharaBadgeItemB"]
},
SentEvoSekharaRankTwo: {
sub: "/Lotus/Language/Inbox/EvolvingSekharaUnlockBName",
sndr: "/Lotus/Language/Bosses/Ordis",
msg: "/Lotus/Language/Inbox/EvolvingSekharaUnlockBDesc",
icon: "/Lotus/Interface/Icons/Npcs/Ordis.png",
att: ["/Lotus/Upgrades/Skins/Clan/ZarimanEvolvingSekharaBadgeItemC"]
}
};
export const addChallenges = async (
account: TAccountDocument,
inventory: TInventoryDatabaseDocument,
ChallengeProgress: IChallengeProgress[],
SeasonChallengeCompletions: ISeasonChallenge[] | undefined
): IAffiliationMods[] => {
ChallengeProgress.forEach(({ Name, Progress }) => {
const itemIndex = inventory.ChallengeProgress.findIndex(i => i.Name === Name);
if (itemIndex !== -1) {
inventory.ChallengeProgress[itemIndex].Progress = Progress;
): Promise<IAffiliationMods[]> => {
for (const { Name, Progress, Completed } of ChallengeProgress) {
let dbChallenge = inventory.ChallengeProgress.find(x => x.Name == Name);
if (dbChallenge) {
dbChallenge.Progress = Progress;
} else {
inventory.ChallengeProgress.push({ Name, Progress });
dbChallenge = { Name, Progress };
inventory.ChallengeProgress.push(dbChallenge);
}
if (Name.startsWith("Calendar")) {
addString(getCalendarProgress(inventory).SeasonProgress.ActivatedChallenges, Name);
}
});
if ((Completed?.length ?? 0) > (dbChallenge.Completed?.length ?? 0)) {
dbChallenge.Completed ??= [];
for (const completion of Completed!) {
if (dbChallenge.Completed.indexOf(completion) == -1) {
dbChallenge.Completed.push(completion);
if (completion == "challengeRewards") {
if (Name in challengeRewardsInboxMessages) {
await createMessage(account._id, [challengeRewardsInboxMessages[Name]]);
// Would love to somehow let the client know about inbox or inventory changes, but there doesn't seem to anything for updateChallengeProgress.
continue;
}
logger.warn(`ignoring unknown challenge completion`, { challenge: Name, completion });
dbChallenge.Completed = [];
}
}
}
} else {
dbChallenge.Completed = Completed;
}
}
const affiliationMods: IAffiliationMods[] = [];
if (SeasonChallengeCompletions) {
@ -1897,7 +2069,7 @@ export const addCalendarProgress = (inventory: TInventoryDatabaseDocument, value
calendarProgress.SeasonProgress.LastCompletedChallengeDayIdx = currentSeason.Days.findIndex(
day => day.events.length != 0 && day.events[0].challenge == value[value.length - 1].challenge
);
checkCalendarChallengeCompletion(calendarProgress, currentSeason);
checkCalendarAutoAdvance(inventory, currentSeason);
};
export const addMissionComplete = (inventory: TInventoryDatabaseDocument, { Tag, Completes, Tier }: IMission): void => {
@ -2045,6 +2217,21 @@ export const cleanupInventory = (inventory: TInventoryDatabaseDocument): void =>
inventory.LotusCustomization.syancol = {};
}
}
{
let numFixed = 0;
for (const equipmentKey of equipmentKeys) {
for (const item of inventory[equipmentKey]) {
if (item.ModularParts?.length === 0) {
item.ModularParts = undefined;
++numFixed;
}
}
}
if (numFixed != 0) {
logger.debug(`removed ModularParts from ${numFixed} non-modular items`);
}
}
};
export const getDialogue = (inventory: TInventoryDatabaseDocument, dialogueName: string): IDialogueDatabase => {
@ -2082,8 +2269,8 @@ export const getCalendarProgress = (inventory: TInventoryDatabaseDocument): ICal
},
SeasonProgress: {
SeasonType: currentSeason.Season,
LastCompletedDayIdx: 0,
LastCompletedChallengeDayIdx: 0,
LastCompletedDayIdx: -1,
LastCompletedChallengeDayIdx: -1,
ActivatedChallenges: []
}
};
@ -2104,16 +2291,44 @@ export const getCalendarProgress = (inventory: TInventoryDatabaseDocument): ICal
return inventory.CalendarProgress;
};
export const checkCalendarChallengeCompletion = (
calendarProgress: ICalendarProgress,
export const checkCalendarAutoAdvance = (
inventory: TInventoryDatabaseDocument,
currentSeason: ICalendarSeason
): void => {
const dayIndex = calendarProgress.SeasonProgress.LastCompletedDayIdx + 1;
if (calendarProgress.SeasonProgress.LastCompletedChallengeDayIdx >= dayIndex) {
const calendarProgress = inventory.CalendarProgress!;
for (
let dayIndex = calendarProgress.SeasonProgress.LastCompletedDayIdx + 1;
dayIndex != currentSeason.Days.length;
++dayIndex
) {
const day = currentSeason.Days[dayIndex];
if (day.events.length != 0 && day.events[0].type == "CET_CHALLENGE") {
if (day.events.length == 0) {
// birthday
if (day.day == 1) {
// kaya
if ((inventory.Affiliations.find(x => x.Tag == "HexSyndicate")?.Title || 0) >= 4) {
break;
}
logger.debug(`cannot talk to kaya, skipping birthday`);
calendarProgress.SeasonProgress.LastCompletedDayIdx++;
} else if (day.day == 74 || day.day == 355) {
// minerva, velimir
if ((inventory.Affiliations.find(x => x.Tag == "HexSyndicate")?.Title || 0) >= 5) {
break;
}
logger.debug(`cannot talk to minerva/velimir, skipping birthday`);
calendarProgress.SeasonProgress.LastCompletedDayIdx++;
} else {
break;
}
} else if (day.events[0].type == "CET_CHALLENGE") {
if (calendarProgress.SeasonProgress.LastCompletedChallengeDayIdx < dayIndex) {
break;
}
//logger.debug(`already completed the challenge, skipping ahead`);
calendarProgress.SeasonProgress.LastCompletedDayIdx++;
} else {
break;
}
}
};

View File

@ -32,7 +32,7 @@ export const getUsernameFromEmail = async (email: string): Promise<string> => {
name = nameFromEmail + suffix;
} while (await isNameTaken(name));
}
return nameFromEmail;
return name;
};
export const createAccount = async (accountData: IDatabaseAccountRequiredFields): Promise<IDatabaseAccountJson> => {

View File

@ -50,7 +50,7 @@ import { getEntriesUnsafe } from "@/src/utils/ts-utils";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { IMissionCredits, IMissionReward } from "@/src/types/missionTypes";
import { crackRelic } from "@/src/helpers/relicHelper";
import { createMessage } from "@/src/services/inboxService";
import { createMessage, IMessageCreationTemplate } from "@/src/services/inboxService";
import kuriaMessage50 from "@/static/fixed_responses/kuriaMessages/fiftyPercent.json";
import kuriaMessage75 from "@/static/fixed_responses/kuriaMessages/seventyFivePercent.json";
import kuriaMessage100 from "@/static/fixed_responses/kuriaMessages/oneHundredPercent.json";
@ -76,13 +76,20 @@ import {
} from "@/src/services/worldStateService";
import { config } from "@/src/services/configService";
import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json";
import { ISyndicateMissionInfo } from "@/src/types/worldStateTypes";
import { IGoal, ISyndicateMissionInfo } from "@/src/types/worldStateTypes";
import { fromOid } from "@/src/helpers/inventoryHelpers";
import { TAccountDocument } from "@/src/services/loginService";
import { ITypeCount } from "@/src/types/commonTypes";
import { IEquipmentClient } from "@/src/types/equipmentTypes";
import { Guild } from "@/src/models/guildModel";
import { handleGuildGoalProgress } from "@/src/services/guildService";
const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[] => {
// Disruption missions just tell us (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/2599)
if (rewardInfo.rewardTierOverrides) {
return rewardInfo.rewardTierOverrides;
}
// For Spy missions, e.g. 3 vaults cracked = A, B, C
if (rewardInfo.VaultsCracked) {
const rotations: number[] = [];
@ -92,14 +99,23 @@ const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[]
return rotations;
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const missionIndex: number | undefined = ExportRegions[rewardInfo.node]?.missionIndex;
const region = ExportRegions[rewardInfo.node] as IRegion | undefined;
const missionIndex: number | undefined = region?.missionIndex;
// For Rescue missions
if (missionIndex == 3 && rewardInfo.rewardTier) {
return [rewardInfo.rewardTier];
}
// 'rewardQualifications' is unreliable for non-endless railjack missions (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/2586, https://onlyg.it/OpenWF/SpaceNinjaServer/issues/2612)
switch (region?.missionName) {
case "/Lotus/Language/Missions/MissionName_Railjack":
case "/Lotus/Language/Missions/MissionName_RailjackVolatile":
case "/Lotus/Language/Missions/MissionName_RailjackExterminate":
case "/Lotus/Language/Missions/MissionName_RailjackAssassinate":
return [0];
}
const rotationCount = rewardInfo.rewardQualifications?.length || 0;
// Empty or absent rewardQualifications should not give rewards when:
@ -292,7 +308,7 @@ export const addMissionInventoryUpdates = async (
addRecipes(inventory, value);
break;
case "ChallengeProgress":
addChallenges(account, inventory, value, inventoryUpdates.SeasonChallengeCompletions);
await addChallenges(account, inventory, value, inventoryUpdates.SeasonChallengeCompletions);
break;
case "FusionTreasures":
addFusionTreasures(inventory, value);
@ -558,6 +574,7 @@ export const addMissionInventoryUpdates = async (
}
]);
}
inventory.DeathSquadable = false;
break;
}
case "LockedWeaponGroup": {
@ -576,7 +593,7 @@ export const addMissionInventoryUpdates = async (
break;
}
case "IncHarvester": {
inventory.Harvestable = true;
// Unsure what to do with this
break;
}
case "CurrentLoadOutIds": {
@ -613,37 +630,103 @@ export const addMissionInventoryUpdates = async (
if (goal && goal.Personal) {
inventory.PersonalGoalProgress ??= [];
const goalProgress = inventory.PersonalGoalProgress.find(x => x.goalId.equals(goal._id.$oid));
if (goalProgress) {
goalProgress.Best = Math.max(goalProgress.Best, uploadProgress.Best);
goalProgress.Count += uploadProgress.Count;
} else {
if (!goalProgress) {
inventory.PersonalGoalProgress.push({
Best: uploadProgress.Best,
Count: uploadProgress.Count,
Tag: goal.Tag,
goalId: new Types.ObjectId(goal._id.$oid)
});
}
const currentNode = inventoryUpdates.RewardInfo!.node;
let currentMissionKey;
if (currentNode == goal.Node) {
currentMissionKey = goal.MissionKeyName;
} else if (goal.ConcurrentNodes && goal.ConcurrentMissionKeyNames) {
for (let i = 0; i < goal.ConcurrentNodes.length; i++) {
if (currentNode == goal.ConcurrentNodes[i]) {
currentMissionKey = goal.ConcurrentMissionKeyNames[i];
break;
}
}
}
if (currentMissionKey && currentMissionKey in goalMessagesByKey) {
const totalCount = (goalProgress?.Count ?? 0) + uploadProgress.Count;
let reward;
if (goal.InterimGoals && goal.InterimRewards) {
for (let i = 0; i < goal.InterimGoals.length; i++) {
if (
goal.InterimGoals[i] &&
goal.InterimGoals[i] <= totalCount &&
(!goalProgress || goalProgress.Count < goal.InterimGoals[i]) &&
goal.InterimRewards[i]
) {
reward = goal.InterimRewards[i];
break;
}
}
}
if (
goal.Reward &&
goal.Reward.items &&
goal.MissionKeyName &&
goal.MissionKeyName in goalMessagesByKey
!reward &&
goal.Goal &&
goal.Goal <= totalCount &&
(!goalProgress || goalProgress.Count < goal.Goal) &&
goal.Reward
) {
// Send reward via inbox
const info = goalMessagesByKey[goal.MissionKeyName];
await createMessage(inventory.accountOwnerId, [
{
reward = goal.Reward;
}
if (
!reward &&
goal.BonusGoal &&
goal.BonusGoal <= totalCount &&
(!goalProgress || goalProgress.Count < goal.BonusGoal) &&
goal.BonusReward
) {
reward = goal.BonusReward;
}
if (reward) {
if (currentMissionKey in goalMessagesByKey) {
// Send reward via inbox
const info = goalMessagesByKey[currentMissionKey];
const message: IMessageCreationTemplate = {
sndr: info.sndr,
msg: info.msg,
att: goal.Reward.items.map(x => (isStoreItem(x) ? fromStoreItem(x) : x)),
sub: info.sub,
icon: info.icon,
highPriority: true
};
if (reward.items) {
message.att = reward.items.map(x => (isStoreItem(x) ? fromStoreItem(x) : x));
}
]);
if (reward.countedItems) {
message.countedAtt = reward.countedItems;
}
if (reward.credits) {
message.RegularCredits = reward.credits;
}
await createMessage(inventory.accountOwnerId, [message]);
}
}
}
if (goalProgress) {
goalProgress.Best = Math.max(goalProgress.Best!, uploadProgress.Best);
goalProgress.Count += uploadProgress.Count;
}
}
if (goal && goal.ClanGoal && inventory.GuildId) {
const guild = await Guild.findById(inventory.GuildId, "GoalProgress Tier VaultDecoRecipes");
if (guild) {
await handleGuildGoalProgress(guild, {
Count: uploadProgress.Count,
Tag: goal.Tag,
goalId: new Types.ObjectId(goal._id.$oid)
});
}
}
}
break;
@ -770,26 +853,24 @@ export const addMissionInventoryUpdates = async (
}
}
if (value.killed) {
await createMessage(inventory.accountOwnerId, [
{
sndr: "/Lotus/Language/Bosses/Ordis",
msg: manifest.messageBody,
arg: [
{
Key: "LICH_NAME",
Tag: value.nemesisName
}
],
att: att,
countedAtt: countedAtt,
attVisualOnly: true,
sub: manifest.messageTitle,
icon: "/Lotus/Interface/Icons/Npcs/Ordis.png",
highPriority: true
}
]);
}
await createMessage(inventory.accountOwnerId, [
{
sndr: value.killed ? "/Lotus/Language/Bosses/Ordis" : value.nemesisName,
msg: value.killed ? manifest.killMessageBody : manifest.convertMessageBody,
arg: [
{
Key: "LICH_NAME",
Tag: value.nemesisName
}
],
att: att,
countedAtt: countedAtt,
attVisualOnly: true,
sub: value.killed ? manifest.killMessageSubject : manifest.convertMessageSubject,
icon: value.killed ? "/Lotus/Interface/Icons/Npcs/Ordis.png" : manifest.convertMessageIcon,
highPriority: true
}
]);
inventory.Nemesis = undefined;
}
@ -971,7 +1052,8 @@ export const addMissionRewards = async (
Missions: missions,
RegularCredits: creditDrops,
VoidTearParticipantsCurrWave: voidTearWave,
StrippedItems: strippedItems
StrippedItems: strippedItems,
AffiliationChanges: AffiliationMods
}: IMissionInventoryUpdateRequest,
firstCompletion: boolean
): Promise<AddMissionRewardsReturnType> => {
@ -991,7 +1073,6 @@ export const addMissionRewards = async (
);
logger.debug("random mission drops:", MissionRewards);
const inventoryChanges: IInventoryChanges = {};
const AffiliationMods: IAffiliationMods[] = [];
let SyndicateXPItemReward;
let ConquestCompletedMissionsCount;
@ -1000,8 +1081,16 @@ export const addMissionRewards = async (
if (rewardInfo.goalId) {
const goal = getWorldState().Goals.find(x => x._id.$oid == rewardInfo.goalId);
if (goal?.MissionKeyName) {
levelKeyName = goal.MissionKeyName;
if (goal) {
if (rewardInfo.node == goal.Node && goal.MissionKeyName) levelKeyName = goal.MissionKeyName;
if (goal.ConcurrentNodes && goal.ConcurrentMissionKeyNames) {
for (let i = 0; i < goal.ConcurrentNodes.length && i < goal.ConcurrentMissionKeyNames.length; i++) {
if (rewardInfo.node == goal.ConcurrentNodes[i]) {
levelKeyName = goal.ConcurrentMissionKeyNames[i];
break;
}
}
}
}
}
@ -1220,6 +1309,27 @@ export const addMissionRewards = async (
logger.error(`unknown droptable ${si.DropTable} for DROP_BLUEPRINT`);
}
}
// e.g. H-09 Apex Turret Sumdali
if (si.DROP_MISC_ITEM) {
const resourceDroptable = droptables.find(x => x.type == "resource");
if (resourceDroptable) {
for (let i = 0; i != si.DROP_MISC_ITEM.length; ++i) {
const reward = getRandomReward(resourceDroptable.items)!;
logger.debug(`stripped droptable (resources pool) rolled`, reward);
if (Object.keys(await addItem(inventory, reward.type)).length == 0) {
logger.debug(`item already owned, skipping`);
} else {
MissionRewards.push({
StoreItem: toStoreItem(reward.type),
ItemCount: 1,
FromEnemyCache: true // to show "identified"
});
}
}
} else {
logger.error(`unknown droptable ${si.DropTable} for DROP_BLUEPRINT`);
}
}
}
}
@ -1253,6 +1363,8 @@ export const addMissionRewards = async (
}
}
AffiliationMods ??= [];
if (rewardInfo.JobStage != undefined && rewardInfo.jobId) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [jobType, unkIndex, hubNode, syndicateMissionId] = rewardInfo.jobId.split("_");
@ -1260,9 +1372,29 @@ export const addMissionRewards = async (
if (syndicateMissionId) {
pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId));
}
const syndicateEntry = syndicateMissions.find(m => m._id.$oid === syndicateMissionId);
let syndicateEntry: ISyndicateMissionInfo | IGoal | undefined = syndicateMissions.find(
m => m._id.$oid === syndicateMissionId
);
if (
[
"/Lotus/Types/Gameplay/Eidolon/Jobs/Events/InfestedPlainsBounty",
"/Lotus/Types/Gameplay/Eidolon/Jobs/Events/GhoulAlertBounty"
].some(prefix => jobType.startsWith(prefix))
) {
const { Goals } = getWorldState(undefined);
syndicateEntry = Goals.find(m => m._id.$oid === syndicateMissionId);
if (syndicateEntry) syndicateEntry.Tag = syndicateEntry.JobAffiliationTag!;
}
if (syndicateEntry && syndicateEntry.Jobs) {
let currentJob = syndicateEntry.Jobs[rewardInfo.JobTier!];
if (
[
"/Lotus/Types/Gameplay/Eidolon/Jobs/Events/InfestedPlainsBounty",
"/Lotus/Types/Gameplay/Eidolon/Jobs/Events/GhoulAlertBounty"
].some(prefix => jobType.startsWith(prefix))
) {
currentJob = syndicateEntry.Jobs.find(j => j.jobType === jobType)!;
}
if (syndicateEntry.Tag === "EntratiSyndicate") {
if (
[
@ -1282,9 +1414,7 @@ export const addMissionRewards = async (
}
}
}
let medallionAmount = Math.floor(
Math.min(rewardInfo.JobStage, currentJob.xpAmounts.length - 1) / (rewardInfo.Q ? 0.8 : 1)
);
let medallionAmount = Math.floor(currentJob.xpAmounts[rewardInfo.JobStage] / (rewardInfo.Q ? 0.8 : 1));
if (
["DeimosEndlessAreaDefenseBounty", "DeimosEndlessExcavateBounty", "DeimosEndlessPurifyBounty"].some(
ending => jobType.endsWith(ending)
@ -1303,35 +1433,45 @@ export const addMissionRewards = async (
ItemCount: medallionAmount
});
SyndicateXPItemReward = medallionAmount;
logger.debug(
`Giving ${medallionAmount} medallions for the ${rewardInfo.JobStage} stage of the ${rewardInfo.JobTier} tier bounty`
);
} else {
if (rewardInfo.JobTier! >= 0) {
const specialCase = [
{ endings: ["Heists/HeistProfitTakerBountyOne"], stage: 2, amount: 1000 },
{ endings: ["Hunts/AllTeralystsHunt"], stage: 2, amount: 5000 },
{
endings: [
"Hunts/TeralystHunt",
"Heists/HeistProfitTakerBountyTwo",
"Heists/HeistProfitTakerBountyThree",
"Heists/HeistProfitTakerBountyFour",
"Heists/HeistExploiterBountyOne"
],
amount: 1000
}
];
const specialCaseReward = specialCase.find(
rule =>
rule.endings.some(e => jobType.endsWith(e)) &&
(rule.stage === undefined || rewardInfo.JobStage === rule.stage)
);
if (specialCaseReward) {
addStanding(inventory, syndicateEntry.Tag, specialCaseReward.amount, AffiliationMods);
} else {
addStanding(
inventory,
syndicateEntry.Tag,
Math.floor(currentJob.xpAmounts[rewardInfo.JobStage] / (rewardInfo.Q ? 0.8 : 1)),
AffiliationMods
);
} else {
if (jobType.endsWith("Heists/HeistProfitTakerBountyOne") && rewardInfo.JobStage === 2) {
addStanding(inventory, syndicateEntry.Tag, 1000, AffiliationMods);
}
if (jobType.endsWith("Hunts/AllTeralystsHunt") && rewardInfo.JobStage === 2) {
addStanding(inventory, syndicateEntry.Tag, 5000, AffiliationMods);
}
if (
[
"Hunts/TeralystHunt",
"Heists/HeistProfitTakerBountyTwo",
"Heists/HeistProfitTakerBountyThree",
"Heists/HeistProfitTakerBountyFour",
"Heists/HeistExploiterBountyOne"
].some(ending => jobType.endsWith(ending))
) {
addStanding(inventory, syndicateEntry.Tag, 1000, AffiliationMods);
}
}
}
}
if (jobType == "/Lotus/Types/Gameplay/Eidolon/Jobs/NewbieJob") {
addStanding(inventory, "CetusSyndicate", Math.floor(200 / (rewardInfo.Q ? 0.8 : 1)), AffiliationMods);
}
}
if (rewardInfo.challengeMissionId) {
@ -1348,6 +1488,7 @@ export const addMissionRewards = async (
ItemCount: medallionAmount
});
SyndicateXPItemReward = medallionAmount;
logger.debug(`Giving ${medallionAmount} medallions for the ${tier} tier bounty`);
} else {
let standingAmount = (tier + 1) * 1000;
if (tier > 5) standingAmount = 7500; // InfestedLichBounty
@ -1636,7 +1777,10 @@ function getRandomMissionDrops(
rewardManifests = [
"/Lotus/Types/Game/MissionDecks/DuviriEncounterRewards/DuviriMurmurFinalSteelChestRewards"
];
} else if (RewardInfo.T == 70) {
} else if (
RewardInfo.T == 70 ||
RewardInfo.T == 6 // https://onlyg.it/OpenWF/SpaceNinjaServer/issues/2526
) {
// Orowyrm chest, gives 10 Pathos Clamps, or 15 on Steel Path.
drops.push({
StoreItem: "/Lotus/StoreItems/Types/Gameplay/Duviri/Resource/DuviriDragonDropItem",
@ -1658,7 +1802,19 @@ function getRandomMissionDrops(
if (syndicateMissionId) {
pushClassicBounties(syndicateMissions, idToBountyCycle(syndicateMissionId));
}
const syndicateEntry = syndicateMissions.find(m => m._id.$oid === syndicateMissionId);
let syndicateEntry: ISyndicateMissionInfo | IGoal | undefined = syndicateMissions.find(
m => m._id.$oid === syndicateMissionId
);
if (
[
"/Lotus/Types/Gameplay/Eidolon/Jobs/Events/InfestedPlainsBounty",
"/Lotus/Types/Gameplay/Eidolon/Jobs/Events/GhoulAlertBounty"
].some(prefix => jobType.startsWith(prefix))
) {
const { Goals } = getWorldState(undefined);
syndicateEntry = Goals.find(m => m._id.$oid === syndicateMissionId);
if (syndicateEntry) syndicateEntry.Tag = syndicateEntry.JobAffiliationTag!;
}
if (syndicateEntry && syndicateEntry.Jobs) {
let job = syndicateEntry.Jobs[RewardInfo.JobTier!];
@ -1743,6 +1899,14 @@ function getRandomMissionDrops(
}
}
}
if (
[
"/Lotus/Types/Gameplay/Eidolon/Jobs/Events/InfestedPlainsBounty",
"/Lotus/Types/Gameplay/Eidolon/Jobs/Events/GhoulAlertBounty"
].some(prefix => jobType.startsWith(prefix))
) {
job = syndicateEntry.Jobs.find(j => j.jobType === jobType)!;
}
rewardManifests = [job.rewards];
if (job.xpAmounts.length > 1) {
const curentStage = RewardInfo.JobStage! + 1;
@ -1770,6 +1934,11 @@ function getRandomMissionDrops(
}
}
}
if (jobType == "/Lotus/Types/Gameplay/Eidolon/Jobs/NewbieJob") {
rewardManifests = ["/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierATableARewards"];
rotations = [3];
if (RewardInfo.Q) rotations.push(3);
}
}
} else if (RewardInfo.challengeMissionId) {
const rewardTables: Record<string, string[]> = {
@ -1866,6 +2035,36 @@ function getRandomMissionDrops(
}
});
// Railjack Abandoned Cache Rewards, Rotation A (Mandatory Objectives)
if (RewardInfo.POICompletions) {
if (region.cacheRewardManifest) {
const deck = ExportRewards[region.cacheRewardManifest];
for (let cache = 0; cache != RewardInfo.POICompletions; ++cache) {
const drop = getRandomRewardByChance(deck[0]);
if (drop) {
drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount, FromEnemyCache: true });
}
}
} else {
logger.error(`POI completed, but there was no cache reward manifest at ${RewardInfo.node}`);
}
}
// Railjack Abandoned Cache Rewards, Rotation B (Optional Objectives)
if (RewardInfo.LootDungeonCompletions) {
if (region.cacheRewardManifest) {
const deck = ExportRewards[region.cacheRewardManifest];
for (let cache = 0; cache != RewardInfo.LootDungeonCompletions; ++cache) {
const drop = getRandomRewardByChance(deck[1]);
if (drop) {
drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount, FromEnemyCache: true });
}
}
} else {
logger.error(`Loot dungeon completed, but there was no cache reward manifest at ${RewardInfo.node}`);
}
}
if (region.cacheRewardManifest && RewardInfo.EnemyCachesFound) {
const deck = ExportRewards[region.cacheRewardManifest];
for (let rotation = 0; rotation != RewardInfo.EnemyCachesFound; ++rotation) {
@ -2079,5 +2278,143 @@ const goalMessagesByKey: Record<string, { sndr: string; msg: string; sub: string
msg: "/Lotus/Language/Messages/GalleonRobbery2025RewardMsgC",
sub: "/Lotus/Language/Messages/GalleonRobbery2025MissionTitleC",
icon: "/Lotus/Interface/Icons/Npcs/VayHekPortrait.png"
},
"/Lotus/Types/Keys/TacAlertKeyWaterFightA": {
sndr: "/Lotus/Language/Bosses/BossKelaDeThaym",
msg: "/Lotus/Language/Inbox/WaterFightRewardMsgA",
sub: "/Lotus/Language/Inbox/WaterFightRewardSubjectA",
icon: "/Lotus/Interface/Icons/Npcs/Grineer/KelaDeThaym.png"
},
"/Lotus/Types/Keys/TacAlertKeyWaterFightB": {
sndr: "/Lotus/Language/Bosses/BossKelaDeThaym",
msg: "/Lotus/Language/Inbox/WaterFightRewardMsgB",
sub: "/Lotus/Language/Inbox/WaterFightRewardSubjectB",
icon: "/Lotus/Interface/Icons/Npcs/Grineer/KelaDeThaym.png"
},
"/Lotus/Types/Keys/TacAlertKeyWaterFightC": {
sndr: "/Lotus/Language/Bosses/BossKelaDeThaym",
msg: "/Lotus/Language/Inbox/WaterFightRewardMsgC",
sub: "/Lotus/Language/Inbox/WaterFightRewardSubjectC",
icon: "/Lotus/Interface/Icons/Npcs/Grineer/KelaDeThaym.png"
},
"/Lotus/Types/Keys/TacAlertKeyWaterFightD": {
sndr: "/Lotus/Language/Bosses/BossKelaDeThaym",
msg: "/Lotus/Language/Inbox/WaterFightRewardMsgD",
sub: "/Lotus/Language/Inbox/WaterFightRewardSubjectD",
icon: "/Lotus/Interface/Icons/Npcs/Grineer/KelaDeThaym.png"
},
"/Lotus/Types/Keys/WolfTacAlertReduxA": {
sndr: "/Lotus/Language/Bosses/NoraNight",
msg: "/Lotus/Language/Inbox/WolfTacAlertBody",
sub: "/Lotus/Language/Inbox/WolfTacAlertTitle",
icon: "/Lotus/Interface/Icons/Npcs/Seasonal/NoraNight.png"
},
"/Lotus/Types/Keys/WolfTacAlertReduxB": {
sndr: "/Lotus/Language/Bosses/NoraNight",
msg: "/Lotus/Language/Inbox/WolfTacAlertBody",
sub: "/Lotus/Language/Inbox/WolfTacAlertTitle",
icon: "/Lotus/Interface/Icons/Npcs/Seasonal/NoraNight.png"
},
"/Lotus/Types/Keys/WolfTacAlertReduxD": {
sndr: "/Lotus/Language/Bosses/NoraNight",
msg: "/Lotus/Language/Inbox/WolfTacAlertBody",
sub: "/Lotus/Language/Inbox/WolfTacAlertTitle",
icon: "/Lotus/Interface/Icons/Npcs/Seasonal/NoraNight.png"
},
"/Lotus/Types/Keys/WolfTacAlertReduxC": {
sndr: "/Lotus/Language/Bosses/NoraNight",
msg: "/Lotus/Language/Inbox/WolfTacAlertBody",
sub: "/Lotus/Language/Inbox/WolfTacAlertTitle",
icon: "/Lotus/Interface/Icons/Npcs/Seasonal/NoraNight.png"
},
"/Lotus/Types/Keys/LanternEndlessEventKeyA": {
sndr: "/Lotus/Language/Bosses/Lotus",
msg: "/Lotus/Language/G1Quests/GenericEventRewardMsgDesc",
sub: "/Lotus/Language/G1Quests/GenericTacAlertRewardMsgTitle",
icon: "/Lotus/Interface/Icons/Npcs/LotusVamp_d.png"
},
"/Lotus/Types/Keys/LanternEndlessEventKeyB": {
sndr: "/Lotus/Language/Bosses/Lotus",
msg: "/Lotus/Language/G1Quests/GenericEventRewardMsgDesc",
sub: "/Lotus/Language/G1Quests/GenericTacAlertRewardMsgTitle",
icon: "/Lotus/Interface/Icons/Npcs/LotusVamp_d.png"
},
"/Lotus/Types/Keys/LanternEndlessEventKeyD": {
sndr: "/Lotus/Language/Bosses/Lotus",
msg: "/Lotus/Language/G1Quests/GenericEventRewardMsgDesc",
sub: "/Lotus/Language/G1Quests/GenericTacAlertRewardMsgTitle",
icon: "/Lotus/Interface/Icons/Npcs/LotusVamp_d.png"
},
"/Lotus/Types/Keys/LanternEndlessEventKeyC": {
sndr: "/Lotus/Language/Bosses/Lotus",
msg: "/Lotus/Language/G1Quests/GenericEventRewardMsgDesc",
sub: "/Lotus/Language/G1Quests/GenericTacAlertRewardMsgTitle",
icon: "/Lotus/Interface/Icons/Npcs/LotusVamp_d.png"
},
"/Lotus/Types/Keys/TacAlertKeyHalloween": {
sndr: "/Lotus/Language/Bosses/Lotus",
msg: "/Lotus/Language/G1Quests/TacAlertHalloweenRewardsBonusBody",
sub: "/Lotus/Language/G1Quests/TacAlertHalloweenRewardsBonusTitle",
icon: "/Lotus/Interface/Icons/Npcs/LotusVamp_d.png"
},
"/Lotus/Types/Keys/TacAlertKeyHalloweenBonus": {
sndr: "/Lotus/Language/Bosses/Lotus",
msg: "/Lotus/Language/G1Quests/TacAlertHalloweenRewardsBody",
sub: "/Lotus/Language/G1Quests/TacAlertHalloweenRewardsTitle",
icon: "/Lotus/Interface/Icons/Npcs/LotusVamp_d.png"
},
"/Lotus/Types/Keys/TacAlertKeyHalloweenTimeAttack": {
sndr: "/Lotus/Language/Bosses/Lotus",
msg: "/Lotus/Language/G1Quests/TacAlertHalloweenRewardsBody",
sub: "/Lotus/Language/G1Quests/TacAlertHalloweenRewardsTitle",
icon: "/Lotus/Interface/Icons/Npcs/LotusVamp_d.png"
},
"/Lotus/Types/Keys/TacAlertKeyProxyRebellionOne": {
sndr: "/Lotus/Language/Bosses/Lotus",
msg: "/Lotus/Language/G1Quests/RazorbackArmadaRewardBody",
sub: "/Lotus/Language/G1Quests/GenericTacAlertSmallRewardMsgTitle",
icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png"
},
"/Lotus/Types/Keys/TacAlertKeyProxyRebellionTwo": {
sndr: "/Lotus/Language/Bosses/Lotus",
msg: "/Lotus/Language/G1Quests/RazorbackArmadaRewardBody",
sub: "/Lotus/Language/G1Quests/GenericTacAlertSmallRewardMsgTitle",
icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png"
},
"/Lotus/Types/Keys/TacAlertKeyProxyRebellionThree": {
sndr: "/Lotus/Language/Bosses/Lotus",
msg: "/Lotus/Language/G1Quests/RazorbackArmadaRewardBody",
sub: "/Lotus/Language/G1Quests/GenericTacAlertSmallRewardMsgTitle",
icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png"
},
"/Lotus/Types/Keys/TacAlertKeyProxyRebellionFour": {
sndr: "/Lotus/Language/Bosses/Lotus",
msg: "/Lotus/Language/G1Quests/GenericTacAlertBadgeRewardMsgDesc",
sub: "/Lotus/Language/G1Quests/GenericTacAlertBadgeRewardMsgTitle",
icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png"
},
"/Lotus/Types/Keys/TacAlertKeyProjectNightwatchEasy": {
sndr: "/Lotus/Language/Bosses/Lotus",
msg: "/Lotus/Language/G1Quests/ProjectNightwatchRewardMsgA",
sub: "/Lotus/Language/G1Quests/ProjectNightwatchTacAlertMissionOneTitle",
icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png"
},
"/Lotus/Types/Keys/TacAlertKeyProjectNightwatch": {
sndr: "/Lotus/Language/Bosses/Lotus",
msg: "/Lotus/Language/G1Quests/ProjectNightwatchTacAlertMissionRewardBody",
sub: "/Lotus/Language/G1Quests/ProjectNightwatchTacAlertMissionTwoTitle",
icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png"
},
"/Lotus/Types/Keys/TacAlertKeyProjectNightwatchHard": {
sndr: "/Lotus/Language/Bosses/Lotus",
msg: "/Lotus/Language/G1Quests/ProjectNightwatchTacAlertMissionRewardBody",
sub: "/Lotus/Language/G1Quests/ProjectNightwatchTacAlertMissionThreeTitle",
icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png"
},
"/Lotus/Types/Keys/TacAlertKeyProjectNightwatchBonus": {
sndr: "/Lotus/Language/Bosses/Lotus",
msg: "/Lotus/Language/G1Quests/ProjectNightwatchTacAlertMissionRewardBody",
sub: "/Lotus/Language/G1Quests/ProjectNightwatchTacAlertMissionFourTitle",
icon: "/Lotus/Interface/Icons/Npcs/Lotus_d.png"
}
};

View File

@ -1,4 +1,4 @@
import { parseSlotPurchaseName } from "@/src/helpers/purchaseHelpers";
import { parseSlotPurchaseName, slotPurchaseNameToSlotName } from "@/src/helpers/purchaseHelpers";
import { getSubstringFromKeyword } from "@/src/helpers/stringHelpers";
import {
addBooster,
@ -14,7 +14,6 @@ import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import {
IPurchaseRequest,
IPurchaseResponse,
SlotPurchase,
IInventoryChanges,
PurchaseSource,
IPurchaseParams
@ -37,6 +36,9 @@ import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/invento
import { fromStoreItem, toStoreItem } from "@/src/services/itemDataService";
import { DailyDeal } from "@/src/models/worldStateModel";
import { fromMongoDate, toMongoDate } from "@/src/helpers/inventoryHelpers";
import { Guild } from "@/src/models/guildModel";
import { handleGuildGoalProgress } from "@/src/services/guildService";
import { Types } from "mongoose";
export const getStoreItemCategory = (storeItem: string): string => {
const storeItemString = getSubstringFromKeyword(storeItem, "StoreItems/");
@ -138,6 +140,22 @@ export const handlePurchase = async (
updateCurrency(inventory, offer.PremiumPrice[0], true, prePurchaseInventoryChanges);
}
}
if (
inventory.GuildId &&
offer.ItemPrices &&
manifest.VendorInfo.TypeName ==
"/Lotus/Types/Game/VendorManifests/Events/DuviriMurmurInvasionVendorManifest"
) {
const guild = await Guild.findById(inventory.GuildId, "GoalProgress Tier VaultDecoRecipes");
const goal = getWorldState().Goals.find(x => x.Tag == "DuviriMurmurEvent");
if (guild && goal) {
await handleGuildGoalProgress(guild, {
Count: offer.ItemPrices[0].ItemCount * purchaseRequest.PurchaseParams.Quantity,
Tag: goal.Tag,
goalId: new Types.ObjectId(goal._id.$oid)
});
}
}
if (!config.dontSubtractPurchaseItemCost) {
if (offer.ItemPrices) {
handleItemPrices(
@ -328,7 +346,7 @@ export const handlePurchase = async (
purchaseResponse.InventoryChanges.MiscItems ??= [];
purchaseResponse.InventoryChanges.MiscItems.push(invItem);
}
} else if (!config.infiniteRegalAya) {
} else if (!inventory.infiniteRegalAya) {
inventory.PrimeTokens -= offer.PrimePrice! * purchaseRequest.PurchaseParams.Quantity;
purchaseResponse.InventoryChanges.PrimeTokens ??= 0;
@ -472,19 +490,6 @@ export const handleStoreItemAcquisition = async (
return purchaseResponse;
};
export const slotPurchaseNameToSlotName: SlotPurchase = {
SuitSlotItem: { name: "SuitBin", purchaseQuantity: 1 },
TwoSentinelSlotItem: { name: "SentinelBin", purchaseQuantity: 2 },
TwoWeaponSlotItem: { name: "WeaponBin", purchaseQuantity: 2 },
SpaceSuitSlotItem: { name: "SpaceSuitBin", purchaseQuantity: 1 },
TwoSpaceWeaponSlotItem: { name: "SpaceWeaponBin", purchaseQuantity: 2 },
MechSlotItem: { name: "MechBin", purchaseQuantity: 1 },
TwoOperatorWeaponSlotItem: { name: "OperatorAmpBin", purchaseQuantity: 2 },
RandomModSlotItem: { name: "RandomModBin", purchaseQuantity: 3 },
TwoCrewShipSalvageSlotItem: { name: "CrewShipSalvageBin", purchaseQuantity: 2 },
CrewMemberSlotItem: { name: "CrewMemberBin", purchaseQuantity: 1 }
};
// // extra = everything above the base +2 slots (depending on slot type)
// // new slot above base = extra + 1 and slots +1
// // new frame = slots -1
@ -581,7 +586,7 @@ const handleBoosterPackPurchase = async (
purchaseResponse.InventoryChanges,
await addItem(inventory, specialItemReward.Item)
);
// TOVERIFY: Is the SpecialItemRewardAttenuation entry removed now?
atten.Atten = 0;
} else {
atten.Atten += specialItemReward.PityIncreaseRate!;
}

View File

@ -236,7 +236,7 @@ const handleQuestCompletion = async (
setupKahlSyndicate(inventory);
}
// Whispers in the Walls is unlocked once The New + Heart of Deimos are completed.
// Whispers in the Walls is unlocked once The New War + Heart of Deimos are completed.
if (
doesQuestCompletionFinishSet(inventory, questKey, [
"/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain",

View File

@ -151,4 +151,57 @@ export class SRng {
arr[lastIdx] = tmp;
}
}
shuffledArray<T>(inarr: readonly T[]): T[] {
const arr = [...inarr];
this.shuffleArray(arr);
return arr;
}
}
export const sequentiallyUniqueRandomElement = <T>(
deck: readonly T[],
idx: number,
lookbehind: number,
seed: number = 0
): T | undefined => {
// This algorithm may modify a shuffle up to index `lookbehind + 1`. It assumes that the last `lookbehind` cards are not adjusted.
if (lookbehind + 1 >= deck.length - lookbehind) {
throw new Error(
`this algorithm cannot guarantee ${lookbehind} unique cards in a row with a deck of size ${deck.length}`
);
}
const iteration = Math.trunc(idx / deck.length);
const card = idx % deck.length;
const currentShuffle = new SRng(mixSeeds(new SRng(iteration).randomInt(0, 100_000), seed)).shuffledArray(deck);
if (card < currentShuffle.length - lookbehind) {
// We are indexing before the end of the deck, so adjustments may be needed to achieve uniqueness.
const window: T[] = [];
{
const previousShuffle = new SRng(
mixSeeds(new SRng(iteration - 1).randomInt(0, 100_000), seed)
).shuffledArray(deck);
for (let i = previousShuffle.length - lookbehind; i != previousShuffle.length; ++i) {
window.push(previousShuffle[i]);
}
}
// From this point on, `window.length == lookbehind` should hold.
for (let i = 0; i != lookbehind; ++i) {
if (window.indexOf(currentShuffle[i]) != -1) {
for (let j = i; ; ++j) {
// `j < currentShuffle.length - lookbehind` should hold.
if (window.indexOf(currentShuffle[j]) == -1) {
const tmp = currentShuffle[j];
currentShuffle[j] = currentShuffle[i];
currentShuffle[i] = tmp;
break;
}
}
}
window.splice(0, 1);
window.push(currentShuffle[i]);
}
}
return currentShuffle[card];
};

View File

@ -1,6 +1,8 @@
import { getPersonalRooms } from "@/src/services/personalRoomsService";
import { getShip } from "@/src/services/shipService";
import {
IResetShipDecorationsRequest,
IResetShipDecorationsResponse,
ISetPlacedDecoInfoRequest,
ISetShipCustomizationsRequest,
IShipDecorationsRequest,
@ -17,6 +19,7 @@ import { Guild } from "@/src/models/guildModel";
import { hasGuildPermission } from "@/src/services/guildService";
import { GuildPermission } from "@/src/types/guildTypes";
import { ExportResources } from "warframe-public-export-plus";
import { convertCustomizationInfo } from "@/src/services/importService";
export const setShipCustomizations = async (
accountId: string,
@ -52,12 +55,7 @@ export const handleSetShipDecorations = async (
): Promise<IShipDecorationsResponse> => {
const personalRooms = await getPersonalRooms(accountId);
const rooms =
placedDecoration.BootLocation == "SHOP"
? personalRooms.TailorShop.Rooms
: placedDecoration.IsApartment
? personalRooms.Apartment.Rooms
: personalRooms.Ship.Rooms;
const rooms = getRoomsForBootLocation(personalRooms, placedDecoration);
const roomToPlaceIn = rooms.find(room => room.Name === placedDecoration.Room);
@ -159,7 +157,6 @@ export const handleSetShipDecorations = async (
if (!config.unlockAllShipDecorations) {
const inventory = await getInventory(accountId);
const itemType = Object.entries(ExportResources).find(arr => arr[1].deco == placedDecoration.Type)![0];
if (placedDecoration.Sockets !== undefined) {
addFusionTreasures(inventory, [{ ItemType: itemType, Sockets: placedDecoration.Sockets, ItemCount: -1 }]);
} else {
@ -192,17 +189,62 @@ export const handleSetShipDecorations = async (
const getRoomsForBootLocation = (
personalRooms: TPersonalRoomsDatabaseDocument,
bootLocation: TBootLocation | undefined
request: { BootLocation?: TBootLocation; IsApartment?: boolean }
): RoomsType[] => {
if (bootLocation == "SHOP") {
if (request.BootLocation == "SHOP") {
return personalRooms.TailorShop.Rooms;
}
if (bootLocation == "APARTMENT") {
if (request.BootLocation == "APARTMENT" || request.IsApartment) {
return personalRooms.Apartment.Rooms;
}
return personalRooms.Ship.Rooms;
};
export const handleResetShipDecorations = async (
accountId: string,
request: IResetShipDecorationsRequest
): Promise<IResetShipDecorationsResponse> => {
const [personalRooms, inventory] = await Promise.all([getPersonalRooms(accountId), getInventory(accountId)]);
const room = getRoomsForBootLocation(personalRooms, request).find(room => room.Name === request.Room);
if (!room) {
throw new Error(`unknown room: ${request.Room}`);
}
for (const deco of room.PlacedDecos) {
const entry = Object.entries(ExportResources).find(arr => arr[1].deco == deco.Type);
if (!entry) {
throw new Error(`unknown deco type: ${deco.Type}`);
}
const [itemType, meta] = entry;
if (meta.capacityCost === undefined) {
throw new Error(`unknown deco type: ${deco.Type}`);
}
// refund item
if (!config.unlockAllShipDecorations) {
if (deco.Sockets !== undefined) {
addFusionTreasures(inventory, [{ ItemType: itemType, Sockets: deco.Sockets, ItemCount: 1 }]);
} else {
addShipDecorations(inventory, [{ ItemType: itemType, ItemCount: 1 }]);
}
}
// refund capacity
room.MaxCapacity += meta.capacityCost;
}
// empty room
room.PlacedDecos.splice(0, room.PlacedDecos.length);
await Promise.all([personalRooms.save(), inventory.save()]);
return {
ResetRoom: request.Room,
ClaimedDecos: [], // Not sure what this is for; the client already implies that the decos were returned to inventory.
NewCapacity: room.MaxCapacity
};
};
export const handleSetPlacedDecoInfo = async (accountId: string, req: ISetPlacedDecoInfoRequest): Promise<void> => {
if (req.GuildId && req.ComponentId) {
const guild = (await Guild.findById(req.GuildId))!;
@ -217,7 +259,7 @@ export const handleSetPlacedDecoInfo = async (accountId: string, req: ISetPlaced
const personalRooms = await getPersonalRooms(accountId);
const room = getRoomsForBootLocation(personalRooms, req.BootLocation).find(room => room.Name === req.Room);
const room = getRoomsForBootLocation(personalRooms, req).find(room => room.Name === req.Room);
if (!room) {
throw new Error(`unknown room: ${req.Room}`);
}
@ -228,6 +270,8 @@ export const handleSetPlacedDecoInfo = async (accountId: string, req: ISetPlaced
}
placedDeco.PictureFrameInfo = req.PictureFrameInfo;
placedDeco.CustomizationInfo = req.CustomizationInfo ? convertCustomizationInfo(req.CustomizationInfo) : undefined;
placedDeco.AnimPoseItem = req.AnimPoseItem;
await personalRooms.save();
};

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@ import { Account } from "@/src/models/loginModel";
import { createAccount, createNonce, getUsernameFromEmail, isCorrectPassword } from "@/src/services/loginService";
import { IDatabaseAccountJson } from "@/src/types/loginTypes";
import { HydratedDocument } from "mongoose";
import { logError } from "@/src/utils/logger";
let wsServer: ws.Server | undefined;
let wssServer: ws.Server | undefined;
@ -43,7 +44,7 @@ export const stopWsServers = (promises: Promise<void>[]): void => {
let lastWsid: number = 0;
interface IWsCustomData extends ws {
id?: number;
id: number;
accountId?: string;
}
@ -88,63 +89,67 @@ const wsOnConnect = (ws: ws, req: http.IncomingMessage): void => {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
ws.on("message", async msg => {
const data = JSON.parse(String(msg)) as IWsMsgFromClient;
if (data.auth) {
let account: IDatabaseAccountJson | null = await Account.findOne({ email: data.auth.email });
if (account) {
if (isCorrectPassword(data.auth.password, account.password)) {
if (!account.Nonce) {
account.ClientType = "webui";
account.Nonce = createNonce();
await (account as HydratedDocument<IDatabaseAccountJson>).save();
try {
const data = JSON.parse(String(msg)) as IWsMsgFromClient;
if (data.auth) {
let account: IDatabaseAccountJson | null = await Account.findOne({ email: data.auth.email });
if (account) {
if (isCorrectPassword(data.auth.password, account.password)) {
if (!account.Nonce) {
account.ClientType = "webui";
account.Nonce = createNonce();
await (account as HydratedDocument<IDatabaseAccountJson>).save();
}
} else {
account = null;
}
} else if (data.auth.isRegister) {
const name = await getUsernameFromEmail(data.auth.email);
account = await createAccount({
email: data.auth.email,
password: data.auth.password,
ClientType: "webui",
LastLogin: new Date(),
DisplayName: name,
Nonce: createNonce()
});
}
if (account) {
(ws as IWsCustomData).accountId = account.id;
ws.send(
JSON.stringify({
auth_succ: {
id: account.id,
DisplayName: account.DisplayName,
Nonce: account.Nonce
}
} satisfies IWsMsgToClient)
);
} else {
account = null;
ws.send(
JSON.stringify({
auth_fail: {
isRegister: data.auth.isRegister
}
} satisfies IWsMsgToClient)
);
}
} else if (data.auth.isRegister) {
const name = await getUsernameFromEmail(data.auth.email);
account = await createAccount({
email: data.auth.email,
password: data.auth.password,
ClientType: "webui",
LastLogin: new Date(),
DisplayName: name,
Nonce: createNonce()
});
}
if (account) {
(ws as IWsCustomData).accountId = account.id;
ws.send(
JSON.stringify({
auth_succ: {
id: account.id,
DisplayName: account.DisplayName,
Nonce: account.Nonce
}
} satisfies IWsMsgToClient)
);
} else {
ws.send(
JSON.stringify({
auth_fail: {
isRegister: data.auth.isRegister
}
} satisfies IWsMsgToClient)
if (data.logout) {
const accountId = (ws as IWsCustomData).accountId;
(ws as IWsCustomData).accountId = undefined;
await Account.updateOne(
{
_id: accountId,
ClientType: "webui"
},
{
Nonce: 0
}
);
}
}
if (data.logout) {
const accountId = (ws as IWsCustomData).accountId;
(ws as IWsCustomData).accountId = undefined;
await Account.updateOne(
{
_id: accountId,
ClientType: "webui"
},
{
Nonce: 0
}
);
} catch (e) {
logError(e as Error, `processing websocket message`);
}
});
};
@ -181,18 +186,24 @@ export const sendWsBroadcastTo = (accountId: string, data: IWsMsgToClient): void
}
};
export const sendWsBroadcastExcept = (wsid: number | undefined, data: IWsMsgToClient): void => {
export const sendWsBroadcastEx = (data: IWsMsgToClient, accountId?: string, excludeWsid?: number): void => {
const msg = JSON.stringify(data);
if (wsServer) {
for (const client of wsServer.clients) {
if ((client as IWsCustomData).id != wsid) {
if (
(!accountId || (client as IWsCustomData).accountId == accountId) &&
(client as IWsCustomData).id != excludeWsid
) {
client.send(msg);
}
}
}
if (wssServer) {
for (const client of wssServer.clients) {
if ((client as IWsCustomData).id != wsid) {
if (
(!accountId || (client as IWsCustomData).accountId == accountId) &&
(client as IWsCustomData).id != excludeWsid
) {
client.send(msg);
}
}

View File

@ -1,6 +1,11 @@
import { Types } from "mongoose";
import { IOid, IMongoDate, IOidWithLegacySupport, ITypeCount } from "@/src/types/commonTypes";
import { IFusionTreasure, IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import {
IFusionTreasure,
IMiscItem,
IGoalProgressDatabase,
IGoalProgressClient
} from "@/src/types/inventoryTypes/inventoryTypes";
import { IPictureFrameInfo } from "@/src/types/personalRoomsTypes";
import { IFriendInfo } from "@/src/types/friendTypes";
@ -23,6 +28,8 @@ export interface IGuildClient {
CrossPlatformEnabled?: boolean;
AutoContributeFromVault?: boolean;
AllianceId?: IOidWithLegacySupport;
GoalProgress?: IGoalProgressClient[];
}
export interface IGuildDatabase {
@ -63,6 +70,8 @@ export interface IGuildDatabase {
TechChanges?: IGuildLogEntryContributable[];
RosterActivity?: IGuildLogEntryRoster[];
ClassChanges?: IGuildLogEntryNumber[];
GoalProgress?: IGoalProgressDatabase[];
}
export interface ILongMOTD {

View File

@ -1,4 +1,3 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Types } from "mongoose";
import { IOid, IMongoDate, IOidWithLegacySupport, ITypeCount } from "@/src/types/commonTypes";
import {
@ -20,6 +19,15 @@ export type InventoryDatabaseEquipment = {
[_ in TEquipmentKey]: IEquipmentDatabase[];
};
// Fields specific to SNS
export interface IAccountCheats {
infiniteCredits?: boolean;
infinitePlatinum?: boolean;
infiniteEndo?: boolean;
infiniteRegalAya?: boolean;
infiniteHelminthMaterials?: boolean;
}
export interface IInventoryDatabase
extends Omit<
IInventoryClient,
@ -62,7 +70,8 @@ export interface IInventoryDatabase
| "PersonalGoalProgress"
| TEquipmentKey
>,
InventoryDatabaseEquipment {
InventoryDatabaseEquipment,
IAccountCheats {
accountOwnerId: Types.ObjectId;
Created: Date;
TrainingDate: Date;
@ -100,7 +109,7 @@ export interface IInventoryDatabase
QualifyingInvasions: IInvasionProgressDatabase[];
LastInventorySync?: Types.ObjectId;
EndlessXP?: IEndlessXpProgressDatabase[];
PersonalGoalProgress?: IPersonalGoalProgressDatabase[];
PersonalGoalProgress?: IGoalProgressDatabase[];
}
export interface IQuestKeyDatabase {
@ -216,6 +225,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
PremiumCredits: number;
PremiumCreditsFree: number;
FusionPoints: number;
CrewShipFusionPoints: number; //Dirac (pre-rework Railjack)
PrimeTokens: number;
SuitBin: ISlots;
WeaponBin: ISlots;
@ -308,7 +318,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
HWIDProtectEnabled?: boolean;
KubrowPetPrints: IKubrowPetPrintClient[];
AlignmentReplay?: IAlignment;
PersonalGoalProgress?: IPersonalGoalProgressClient[];
PersonalGoalProgress?: IGoalProgressClient[];
ThemeStyle: string;
ThemeBackground: string;
ThemeSounds: string;
@ -520,7 +530,8 @@ export enum InventorySlot {
SENTINELS = "SentinelBin",
AMPS = "OperatorAmpBin",
RJ_COMPONENT_AND_ARMAMENTS = "CrewShipSalvageBin",
CREWMEMBERS = "CrewMemberBin"
CREWMEMBERS = "CrewMemberBin",
RIVENS = "RandomModBin"
}
export interface ISlots {
@ -719,7 +730,7 @@ export enum UpgradeType {
export interface ILoreFragmentScan {
Progress: number;
Region?: string;
Region: string;
ItemType: string;
}
@ -884,8 +895,8 @@ export interface IPeriodicMissionCompletionResponse extends Omit<IPeriodicMissio
date: IMongoDate;
}
export interface IPersonalGoalProgressClient {
Best: number;
export interface IGoalProgressClient {
Best?: number;
Count: number;
Tag: string;
_id: IOid;
@ -893,7 +904,7 @@ export interface IPersonalGoalProgressClient {
//ReceivedClanReward1?: boolean;
}
export interface IPersonalGoalProgressDatabase extends Omit<IPersonalGoalProgressClient, "_id"> {
export interface IGoalProgressDatabase extends Omit<IGoalProgressClient, "_id"> {
goalId: Types.ObjectId;
}

View File

@ -23,12 +23,19 @@ export interface IMissionCredits {
DailyMissionBonus?: boolean;
}
export interface IMissionInventoryUpdateResponse extends Partial<IMissionCredits> {
export interface IMissionInventoryUpdateResponseRailjackInterstitial extends Partial<IMissionCredits> {
ConquestCompletedMissionsCount?: number;
InventoryJson?: string;
MissionRewards?: IMissionReward[];
InventoryChanges?: IInventoryChanges;
FusionPoints?: number;
SyndicateXPItemReward?: number;
AffiliationMods?: IAffiliationMods[];
}
export interface IMissionInventoryUpdateResponse extends IMissionInventoryUpdateResponseRailjackInterstitial {
InventoryJson?: string;
}
export interface IMissionInventoryUpdateResponseBackToDryDock {
InventoryJson: string;
}

View File

@ -1,6 +1,6 @@
import { IColor, IShipAttachments, IShipCustomization } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { Document, Model, Types } from "mongoose";
import { ILoadoutClient } from "@/src/types/saveLoadoutTypes";
import { ILoadoutClient, ILoadoutConfigClient, ILoadoutConfigDatabase } from "@/src/types/saveLoadoutTypes";
import { IMongoDate, IOid } from "@/src/types/commonTypes";
export interface IGetShipResponse {
@ -17,7 +17,7 @@ export interface IOrbiterClient {
Features: string[];
ShipId: IOid;
ShipInterior: IShipCustomization;
Rooms: IRoom[];
Rooms: IRoomClient[];
VignetteFish?: string[];
FavouriteLoadoutId?: IOid;
Wallpaper?: string;
@ -28,7 +28,7 @@ export interface IOrbiterClient {
export interface IOrbiterDatabase {
Features: string[];
Rooms: IRoom[];
Rooms: IRoomDatabase[];
ShipInterior?: IShipCustomization;
VignetteFish?: string[];
FavouriteLoadoutId?: Types.ObjectId;
@ -53,12 +53,18 @@ export interface IPersonalRoomsDatabase {
TailorShop: ITailorShopDatabase;
}
export interface IRoom {
export interface IRoomDatabase {
Name: string;
MaxCapacity: number;
PlacedDecos?: IPlacedDecosDatabase[];
}
export interface IRoomClient {
Name: string;
MaxCapacity: number;
PlacedDecos?: IPlacedDecosClient[];
}
export interface IPlantClient {
PlantType: string;
EndTime: IMongoDate;
@ -89,14 +95,18 @@ export interface IGardeningDatabase {
export interface IApartmentClient {
Gardening: IGardeningClient;
Rooms: IRoom[];
FavouriteLoadouts: IFavouriteLoadout[];
Rooms: IRoomClient[];
FavouriteLoadouts?: IFavouriteLoadout[];
VideoWallBackdrop?: string;
Soundscape?: string;
}
export interface IApartmentDatabase {
Gardening: IGardeningDatabase;
Rooms: IRoom[];
Rooms: IRoomDatabase[];
FavouriteLoadouts: IFavouriteLoadoutDatabase[];
VideoWallBackdrop?: string;
Soundscape?: string;
}
export interface IPlacedDecosDatabase {
@ -106,11 +116,14 @@ export interface IPlacedDecosDatabase {
Scale?: number;
Sockets?: number;
PictureFrameInfo?: IPictureFrameInfo;
CustomizationInfo?: ICustomizationInfoDatabase;
AnimPoseItem?: string;
_id: Types.ObjectId;
}
export interface IPlacedDecosClient extends Omit<IPlacedDecosDatabase, "_id"> {
export interface IPlacedDecosClient extends Omit<IPlacedDecosDatabase, "_id" | "CustomizationInfo"> {
id: IOid;
CustomizationInfo?: ICustomizationInfoClient;
}
export interface ISetShipCustomizationsRequest {
@ -150,12 +163,25 @@ export interface IShipDecorationsResponse {
NewRoom?: string;
}
export interface IResetShipDecorationsRequest {
Room: string;
BootLocation?: TBootLocation;
}
export interface IResetShipDecorationsResponse {
ResetRoom: string;
ClaimedDecos: [];
NewCapacity: number;
}
export interface ISetPlacedDecoInfoRequest {
DecoType: string;
DecoType?: string;
DecoId: string;
Room: string;
PictureFrameInfo: IPictureFrameInfo;
PictureFrameInfo: IPictureFrameInfo; // IsPicture
CustomizationInfo?: ICustomizationInfoClient; // !IsPicture
BootLocation?: TBootLocation;
AnimPoseItem?: string; // !IsPicture
ComponentId?: string;
GuildId?: string;
}
@ -176,6 +202,21 @@ export interface IPictureFrameInfo {
TextOrientation: number;
}
export interface ICustomizationInfoClient {
Anim?: string;
AnimPose?: number;
LoadOutPreset?: ILoadoutConfigClient;
VehiclePreset?: ILoadoutConfigClient;
EquippedWeapon?: "SUIT_SLOT" | "LONG_GUN_SLOT" | "PISTOL_SLOT";
AvatarType?: string;
LoadOutType?: string; // "LOT_NORMAL"
}
export interface ICustomizationInfoDatabase extends Omit<ICustomizationInfoClient, "LoadOutPreset" | "VehiclePreset"> {
LoadOutPreset?: ILoadoutConfigDatabase;
VehiclePreset?: ILoadoutConfigDatabase;
}
export interface IFavouriteLoadout {
Tag: string;
LoadoutId: IOid;
@ -191,11 +232,12 @@ export interface ITailorShopDatabase {
Colors?: IColor;
CustomJson?: string;
LevelDecosVisible?: boolean;
Rooms: IRoom[];
Rooms: IRoomDatabase[];
}
export interface ITailorShop extends Omit<ITailorShopDatabase, "FavouriteLoadouts"> {
FavouriteLoadouts: IFavouriteLoadout[];
export interface ITailorShop extends Omit<ITailorShopDatabase, "Rooms" | "FavouriteLoadouts"> {
Rooms: IRoomClient[];
FavouriteLoadouts?: IFavouriteLoadout[];
}
export type RoomsType = { Name: string; MaxCapacity: number; PlacedDecos: Types.DocumentArray<IPlacedDecosDatabase> };

View File

@ -8,7 +8,8 @@ import {
IRecentVendorPurchaseClient,
TEquipmentKey,
ICrewMemberClient,
IKubrowPetPrintClient
IKubrowPetPrintClient,
IUpgradeClient
} from "@/src/types/inventoryTypes/inventoryTypes";
export enum PurchaseSource {
@ -73,6 +74,7 @@ export type IInventoryChanges = {
InfestedFoundry?: IInfestedFoundryClient;
Drones?: IDroneClient[];
MiscItems?: IMiscItem[];
ShipDecorations?: ITypeCount[];
EmailItems?: ITypeCount[];
CrewShipRawSalvage?: ITypeCount[];
Nemesis?: Partial<INemesisClient>;
@ -80,6 +82,7 @@ export type IInventoryChanges = {
RecentVendorPurchases?: IRecentVendorPurchaseClient; // < 38.5.0
CrewMembers?: ICrewMemberClient[];
KubrowPetPrints?: IKubrowPetPrintClient[];
Upgrades?: IUpgradeClient[]; // TOVERIFY
} & Record<
Exclude<
string,

View File

@ -117,6 +117,7 @@ export type IMissionInventoryUpdateRequest = {
DropTable: string;
DROP_MOD?: number[];
DROP_BLUEPRINT?: number[];
DROP_MISC_ITEM?: number[];
}[];
DeathMarks?: string[];
Nemesis?: number;
@ -148,6 +149,7 @@ export type IMissionInventoryUpdateRequest = {
MultiProgress: unknown[];
}[];
InvasionProgress?: IInvasionProgressClient[];
RJ?: boolean;
ConquestMissionsCompleted?: number;
duviriSuitSelection?: string;
duviriPistolSelection?: string;
@ -184,7 +186,10 @@ export interface IRewardInfo {
NemesisHintProgress?: number;
EOM_AFK?: number;
rewardQualifications?: string; // did a Survival for 5 minutes and this was "1"
rewardTierOverrides?: number[]; // Disruption
PurgatoryRewardQualifications?: string;
POICompletions?: number;
LootDungeonCompletions?: number;
rewardSeed?: number | bigint;
periodicMissionTag?: string;
T?: number; // Duviri

View File

@ -79,6 +79,7 @@ export interface ILoadoutDatabase {
NORMAL_PVP: ILoadoutConfigDatabase[];
LUNARO: ILoadoutConfigDatabase[];
OPERATOR: ILoadoutConfigDatabase[];
GEAR: ILoadoutConfigDatabase[];
KDRIVE: ILoadoutConfigDatabase[];
DATAKNIFE: ILoadoutConfigDatabase[];
MECH: ILoadoutConfigDatabase[];

View File

@ -5,13 +5,16 @@ export interface IWorldState {
Version: number; // for goals
BuildLabel: string;
Time: number;
InGameMarket: IInGameMarket;
Goals: IGoal[];
Alerts: [];
Sorties: ISortie[];
LiteSorties: ILiteSortie[];
SyndicateMissions: ISyndicateMissionInfo[];
ActiveMissions: IFissure[];
FlashSales: IFlashSale[];
GlobalUpgrades: IGlobalUpgrade[];
Invasions: IInvasion[];
NodeOverrides: INodeOverride[];
VoidTraders: IVoidTrader[];
PrimeVaultTraders: IPrimeVaultTrader[];
@ -36,19 +39,73 @@ export interface IGoal {
_id: IOid;
Activation: IMongoDate;
Expiry: IMongoDate;
Count: number;
Goal: number;
Success: number;
Personal: boolean;
Bounty?: boolean;
ClampNodeScores?: boolean;
Count?: number;
HealthPct?: number;
Icon: string;
Desc: string;
ToolTip?: string;
Icon: string;
Faction?: string;
Goal?: number;
InterimGoals?: number[];
BonusGoal?: number;
ClanGoal?: number[];
Success?: number;
Personal?: boolean;
Community?: boolean;
Best?: boolean; // Fist one on Event Tab
Bounty?: boolean; // Tactical Alert
ClampNodeScores?: boolean;
Transmission?: string;
InstructionalItem?: string;
ItemType?: string;
Tag: string;
Node: string;
PrereqGoalTags?: string[];
Node?: string;
VictimNode?: string;
ConcurrentMissionKeyNames?: string[];
ConcurrentNodeReqs?: number[];
ConcurrentNodes?: string[];
RegionIdx?: number;
Regions?: number[];
MissionKeyName?: string;
Reward?: IMissionReward;
InterimRewards?: IMissionReward[];
BonusReward?: IMissionReward;
JobAffiliationTag?: string;
Jobs?: ISyndicateJob[];
PreviousJobs?: ISyndicateJob[];
JobCurrentVersion?: IOid;
JobPreviousVersion?: IOid;
ScoreVar?: string;
ScoreMaxTag?: string;
ScoreLocTag?: string;
NightLevel?: string;
}
export interface ISyndicateJob {
jobType?: string;
rewards: string;
masteryReq?: number;
minEnemyLevel: number;
maxEnemyLevel: number;
xpAmounts: number[];
endless?: boolean;
locationTag?: string;
isVault?: boolean;
requiredItems?: string[];
useRequiredItemsAsMiscItemFee?: boolean;
}
export interface ISyndicateMissionInfo {
@ -58,17 +115,7 @@ export interface ISyndicateMissionInfo {
Tag: string;
Seed: number;
Nodes: string[];
Jobs?: {
jobType?: string;
rewards: string;
masteryReq: number;
minEnemyLevel: number;
maxEnemyLevel: number;
xpAmounts: number[];
endless?: boolean;
locationTag?: string;
isVault?: boolean;
}[];
Jobs?: ISyndicateJob[];
}
export interface IGlobalUpgrade {
@ -82,6 +129,28 @@ export interface IGlobalUpgrade {
LocalizeDescTag: string;
}
export interface IInvasion {
_id: IOid;
Faction: string;
DefenderFaction: string;
Node: string;
Count: number;
Goal: number;
LocTag: string;
Completed: boolean;
ChainID: IOid;
AttackerReward: IMissionReward;
AttackerMissionInfo: IInvasionMissionInfo;
DefenderReward: IMissionReward;
DefenderMissionInfo: IInvasionMissionInfo;
Activation: IMongoDate;
}
export interface IInvasionMissionInfo {
seed: number;
faction: string;
}
export interface IFissure {
_id: IOid;
Region: number;
@ -242,6 +311,7 @@ export interface IEndlessXpChoice {
export interface ISeasonChallenge {
_id: IOid;
Daily?: boolean;
Permanent?: boolean; // only for getPastWeeklyChallenges response
Activation: IMongoDate;
Expiry: IMongoDate;
Challenge: string;
@ -280,6 +350,37 @@ export type TCircuitGameMode =
| "Assassination"
| "Alchemy";
export interface IFlashSale {
TypeName: string;
ShowInMarket: boolean;
HideFromMarket: boolean;
SupporterPack: boolean;
Discount: number;
BogoBuy: number;
BogoGet: number;
PremiumOverride: number;
RegularOverride: number;
ProductExpiryOverride?: IMongoDate;
StartDate: IMongoDate;
EndDate: IMongoDate;
}
export interface IInGameMarket {
LandingPage: ILandingPage;
}
export interface ILandingPage {
Categories: IGameMarketCategory[];
}
export interface IGameMarketCategory {
CategoryName: string;
Name: string;
Icon: string;
AddToMenu?: boolean;
Items?: string[];
}
export interface ITmp {
cavabegin: string;
PurchasePlatformLockEnabled: boolean; // Seems unused

View File

@ -108,3 +108,13 @@ errorLog.on("new", filename => logger.info(`Using error log file: ${filename}`))
combinedLog.on("new", filename => logger.info(`Using combined log file: ${filename}`));
errorLog.on("rotate", filename => logger.info(`Rotated error log file: ${filename}`));
combinedLog.on("rotate", filename => logger.info(`Rotated combined log file: ${filename}`));
export const logError = (err: Error, context: string): void => {
if (err.stack) {
const stackArr = err.stack.split("\n");
stackArr[0] += ` while ${context}`;
logger.error(stackArr.join("\n"));
} else {
logger.error(`uncaught error while ${context}: ${err.message}`);
}
};

View File

@ -3,3 +3,5 @@ type Entries<T, K extends keyof T = keyof T> = (K extends unknown ? [K, T[K]] :
export function getEntriesUnsafe<T extends object>(object: T): Entries<T> {
return Object.entries(object) as Entries<T>;
}
export const exhaustive = (_: never): void => {};

View File

@ -135,5 +135,6 @@
"/Lotus/Language/EntratiLab/EntratiGeneral/HumanLoidLoved",
"ConquestSetupIntro",
"EntratiLabConquestHardModeUnlocked",
"/Lotus/Language/Npcs/KonzuPostNewWar"
"/Lotus/Language/Npcs/KonzuPostNewWar",
"/Lotus/Language/SolarisVenus/EudicoPostNewWar"
]

View File

@ -16,6 +16,10 @@
{
"ItemType": "/Lotus/Types/Keys/1999PrologueQuest/1999PrologueQuestKeyChain",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Items/EmailItems/TennokaiEmailItem",
"ItemCount": 1
}
]
}

View File

@ -304,7 +304,6 @@
{ "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/Seasonal/AvatarImageGlyphCookieKubrow", "PrimePrice": 80, "RegularPrice": 50000 },
{ "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/LisetScarf", "PrimePrice": 600, "RegularPrice": 400000 },
{ "ItemType": "/Lotus/StoreItems/Types/StoreItems/SuitCustomizations/ColourPickerTwitchBItemA", "PrimePrice": 220, "RegularPrice": 220000 },
{ "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Effects/FootstepsMaple", "PrimePrice": 15, "RegularPrice": 1000 },
{ "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Clan/BaroKavatBadgeItem", "PrimePrice": 50, "RegularPrice": 50000 },
{ "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sigils/BaroKavatSigil", "PrimePrice": 55, "RegularPrice": 45000 },
{ "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Scarves/WraithTurbinesScarf", "PrimePrice": 400, "RegularPrice": 500000 },
@ -363,7 +362,6 @@
{ "ItemType": "/Lotus/StoreItems/Types/Items/MiscItems/PhotoboothTileInarosTomb", "PrimePrice": 325, "RegularPrice": 175000 },
{ "ItemType": "/Lotus/StoreItems/Types/Restoratives/Consumable/BaroFireWorksCrate", "PrimePrice": 50, "RegularPrice": 100000 },
{ "ItemType": "/Lotus/StoreItems/Types/Items/MiscItems/PhotoboothTileOrokinExtraction", "PrimePrice": 325, "RegularPrice": 175000 },
{ "ItemType": "/Lotus/StoreItems/Types/Keys/MummyQuestKeyBlueprint", "PrimePrice": 100, "RegularPrice": 25000 },
{ "ItemType": "/Lotus/StoreItems/Types/Restoratives/Consumable/AssassinBait", "PrimePrice": 200, "RegularPrice": 125000 },
{ "ItemType": "/Lotus/StoreItems/Types/Restoratives/Consumable/AssassinBaitB", "PrimePrice": 200, "RegularPrice": 125000 },
{ "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/BaroKiTeerDecorationB", "PrimePrice": 100, "RegularPrice": 100000 },
@ -401,7 +399,39 @@
{ "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/BaroKiTeerDecorationC", "PrimePrice": 100, "RegularPrice": 100000 },
{ "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/PedistalPrime", "PrimePrice": 0, "RegularPrice": 1000000 },
{ "ItemType": "/Lotus/StoreItems/Types/Items/Emotes/BaroEmote", "PrimePrice": 0, "RegularPrice": 1000000 },
{ "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/EventSniperReloadDamageMod", "PrimePrice": 2995, "RegularPrice": 1000000 }
{ "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/EventSniperReloadDamageMod", "PrimePrice": 2995, "RegularPrice": 1000000 },
{ "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageAvaClemCommunityGlyph", "PrimePrice": 20, "RegularPrice": 33333 },
{ "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/TennoconConcert2025Display", "PrimePrice": 90, "RegularPrice": 125000 },
{ "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/SummerGameFestPoster", "PrimePrice": 90, "RegularPrice": 125000 },
{ "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/RathuumEventPoster", "PrimePrice": 90, "RegularPrice": 125000 },
{ "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/Factions/GlyphFactionCorpus", "PrimePrice": 70, "RegularPrice": 55000 },
{ "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/Factions/GlyphFactionEntrati", "PrimePrice": 99, "RegularPrice": 1900 },
{ "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/Factions/GlyphFactionScaldra", "PrimePrice": 93, "RegularPrice": 1906 },
{ "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/Factions/GlyphFactionTechrot", "PrimePrice": 98, "RegularPrice": 1901 },
{ "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/Warframes/VorunaActionGlyph", "PrimePrice": 75, "RegularPrice": 60000 },
{ "ItemType": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageVoidAngelBaro", "PrimePrice": 80, "RegularPrice": 50000 },
{ "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sigils/1999DrippySigil", "PrimePrice": 50, "RegularPrice": 45000 },
{ "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Weapons/Rapier/CrpRapierSkin", "PrimePrice": 375, "RegularPrice": 400000 },
{ "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Events/OgrisOldSchool", "PrimePrice": 350, "RegularPrice": 325000 },
{ "ItemType": "/Lotus/StoreItems/Upgrades/Skins/Sigils/PrismaLotusFlamesSigil", "PrimePrice": 55, "RegularPrice": 60000 },
{ "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Melee/Expert/WeaponMeleeFactionDamageMurmursExpert", "PrimePrice": 375, "RegularPrice": 130000 },
{ "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/Expert/WeaponRecoilReductionModExpert", "PrimePrice": 300, "RegularPrice": 220000 },
{ "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Rifle/Expert/WeaponRecoilReductionModExpert", "PrimePrice": 300, "RegularPrice": 220000 },
{ "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/Expert/WeaponRecoilReductionModExpert", "PrimePrice": 300, "RegularPrice": 220000 },
{ "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/TenthAnniversaryLoginSongItem", "PrimePrice": 145, "RegularPrice": 165000 },
{ "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/AbyssofDagathSongItem", "PrimePrice": 150, "RegularPrice": 155000 },
{ "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/ZarimanLoginSongItem", "PrimePrice": 160, "RegularPrice": 180000 },
{ "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/DanteUnboundLoginSongItem", "PrimePrice": 150, "RegularPrice": 150000 },
{ "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/EmpyreanSongItem", "PrimePrice": 160, "RegularPrice": 155000 },
{ "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/DeimosLoginSongItem", "PrimePrice": 155, "RegularPrice": 160000 },
{ "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/JadeShadowsLoginSongItem", "PrimePrice": 150, "RegularPrice": 170000 },
{ "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/WhispersInTheWallLoginSongItem", "PrimePrice": 165, "RegularPrice": 170000 },
{ "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/CorpusRailjackLoginSongItem", "PrimePrice": 150, "RegularPrice": 165000 },
{ "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/LotusEatersSongItem", "PrimePrice": 165, "RegularPrice": 150000 },
{ "ItemType": "/Lotus/StoreItems/Types/Items/SongItems/KuvaLichLoginSongItem", "PrimePrice": 140, "RegularPrice": 170000 },
{ "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyBaro", "PrimePrice": 100, "RegularPrice": 125000 },
{ "ItemType": "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyInaros", "PrimePrice": 120, "RegularPrice": 90000 },
{ "ItemType": "/Lotus/StoreItems/Upgrades/Mods/Pistol/Expert/WeaponPistolFactionDamageMurmursExpert", "PrimePrice": 375, "RegularPrice": 130000 }
],
"allIfAny": [
[

View File

@ -0,0 +1,114 @@
{
"FC_CORPUS": [
"SettlementNode1",
"SettlementNode2",
"SettlementNode3",
"SettlementNode11",
"SettlementNode12",
"SettlementNode14",
"SettlementNode15",
"SettlementNode20",
"SolNode1",
"SolNode2",
"SolNode4",
"SolNode6",
"SolNode10",
"SolNode17",
"SolNode21",
"SolNode22",
"SolNode23",
"SolNode25",
"SolNode38",
"SolNode43",
"SolNode48",
"SolNode49",
"SolNode51",
"SolNode53",
"SolNode56",
"SolNode57",
"SolNode61",
"SolNode62",
"SolNode65",
"SolNode66",
"SolNode72",
"SolNode73",
"SolNode74",
"SolNode76",
"SolNode78",
"SolNode81",
"SolNode84",
"SolNode88",
"SolNode97",
"SolNode100",
"SolNode101",
"SolNode102",
"SolNode104",
"SolNode107",
"SolNode109",
"SolNode118",
"SolNode121",
"SolNode123",
"SolNode125",
"SolNode126",
"SolNode127",
"SolNode128",
"SolNode203",
"SolNode205",
"SolNode209",
"SolNode210",
"SolNode211",
"SolNode212",
"SolNode214",
"SolNode216",
"SolNode217",
"SolNode220"
],
"FC_GRINEER": [
"SolNode11",
"SolNode16",
"SolNode18",
"SolNode19",
"SolNode20",
"SolNode30",
"SolNode31",
"SolNode32",
"SolNode36",
"SolNode41",
"SolNode42",
"SolNode45",
"SolNode46",
"SolNode50",
"SolNode58",
"SolNode67",
"SolNode68",
"SolNode70",
"SolNode82",
"SolNode93",
"SolNode96",
"SolNode99",
"SolNode106",
"SolNode113",
"SolNode131",
"SolNode132",
"SolNode135",
"SolNode137",
"SolNode138",
"SolNode139",
"SolNode140",
"SolNode141",
"SolNode144",
"SolNode146",
"SolNode147",
"SolNode149",
"SolNode177",
"SolNode181",
"SolNode184",
"SolNode185",
"SolNode187",
"SolNode188",
"SolNode189",
"SolNode191",
"SolNode195",
"SolNode196"
]
}

View File

@ -0,0 +1,190 @@
{
"FC_GRINEER": {
"COMMON": [
{
"ItemType": "/Lotus/Types/Items/Research/ChemComponent",
"ItemCount": 3
}
],
"UNCOMMON": [
{
"ItemType": "/Lotus/Types/Recipes/Weapons/KarakWraithBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/KarakWraithBarrel",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/KarakWraithReceiver",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/KarakWraithStock",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/StrunWraithBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/StrunWraithBarrel",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/StrunWraithReceiver",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/StrunWraithStock",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/LatronWraithBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/LatronWraithBarrel",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/LatronWraithReceiver",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/LatronWraithStock",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/TwinVipersWraithBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/TwinVipersWraithBarrel",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/TwinVipersWraithLink",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/TwinVipersWraithReceiver",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/GrineerCombatKnifeSortieBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/GrineerCombatKnifeHilt",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/GrineerCombatKnifeBlade",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/GrineerCombatKnifeHeatsink",
"ItemCount": 1
}
],
"RARE": [
{
"ItemType": "/Lotus/Types/Recipes/Components/OrokinCatalystBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Components/OrokinReactorBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Components/FormaBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Components/UtilityUnlockerBlueprint",
"ItemCount": 1
}
]
},
"FC_CORPUS": {
"COMMON": [
{
"ItemType": "/Lotus/Types/Items/Research/EnergyComponent",
"ItemCount": 3
}
],
"UNCOMMON": [
{
"ItemType": "/Lotus/Types/Recipes/Weapons/DeraVandalBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/DeraVandalBarrel",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/DeraVandalReceiver",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/DeraVandalStock",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/SnipetronVandalBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/SnipetronVandalStock",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/SnipetronVandalReceiver",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/SnipetronVandalBarrel",
"ItemCount": 1
}
],
"RARE": [
{
"ItemType": "/Lotus/Types/Recipes/Components/OrokinCatalystBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Components/OrokinReactorBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Components/FormaBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Components/UtilityUnlockerBlueprint",
"ItemCount": 1
}
]
},
"FC_INFESTATION": {
"COMMON": [
{
"ItemType": "/Lotus/Types/Items/Research/BioComponent",
"ItemCount": 1
}
],
"UNCOMMON": [
{
"ItemType": "/Lotus/Types/Items/Research/BioComponent",
"ItemCount": 2
}
],
"RARE": [
{
"ItemType": "/Lotus/Types/Items/MiscItems/InfestedAladCoordinate",
"ItemCount": 1
}
]
}
}

View File

@ -0,0 +1,290 @@
{
"/Lotus/PVPChallengeTypes/PVPTimedChallengeFlagCaptureEASY": {
"ScriptParamValue": 1,
"PVPModeAllowed": ["PVPMODE_CAPTURETHEFLAG"],
"SyndicateXP": 500
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeFlagCaptureMEDIUM": {
"ScriptParamValue": 4,
"PVPModeAllowed": ["PVPMODE_CAPTURETHEFLAG"],
"SyndicateXP": 1500
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeFlagReturnEASY": {
"ScriptParamValue": 1,
"PVPModeAllowed": ["PVPMODE_CAPTURETHEFLAG"],
"SyndicateXP": 500
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsComboEASY": {
"ScriptParamValue": 1,
"PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 500
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsComboMEDIUM": {
"ScriptParamValue": 4,
"PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 1500
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsHeadShotsEASY": {
"ScriptParamValue": 1,
"PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 500
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsHeadShotsMEDIUM": {
"ScriptParamValue": 4,
"PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 1500
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsMeleeEASY": {
"ScriptParamValue": 1,
"PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 500
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsMeleeMEDIUM": {
"ScriptParamValue": 4,
"PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 1500
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsMeleeHARD": {
"ScriptParamValue": 3,
"PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 3000,
"DuringSingleMatch": true
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsMultiMEDIUM": {
"ScriptParamValue": 4,
"PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 1500
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsPaybackEASY": {
"ScriptParamValue": 1,
"PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 500
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsPayback_MEDIUM": {
"ScriptParamValue": 3,
"PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 1500
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsPowerEASY": {
"ScriptParamValue": 1,
"PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 500
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsPowerMEDIUM": {
"ScriptParamValue": 4,
"PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 1500
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsPowerHARD": {
"ScriptParamValue": 3,
"PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 3000,
"DuringSingleMatch": true
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsPrimaryEASY": {
"ScriptParamValue": 1,
"PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 500
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsPrimaryMEDIUM": {
"ScriptParamValue": 4,
"PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 1500
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsPrimaryHARD": {
"ScriptParamValue": 3,
"PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 3000,
"DuringSingleMatch": true
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsSecondaryEASY": {
"ScriptParamValue": 1,
"PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 500
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsSecondaryMEDIUM": {
"ScriptParamValue": 4,
"PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 1500
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsSecondaryHARD": {
"ScriptParamValue": 3,
"PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 3000,
"DuringSingleMatch": true
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsStreak_MEDIUM": {
"ScriptParamValue": 3,
"PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 1500
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsStreakDominationEASY": {
"ScriptParamValue": 1,
"PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 500
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsStreakDomination_MEDIUM": {
"ScriptParamValue": 3,
"PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 1500
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsStreakDominationHARD": {
"ScriptParamValue": 3,
"PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 3000,
"DuringSingleMatch": true
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsStreakStoppedEASY": {
"ScriptParamValue": 1,
"PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 500
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsStreakStopped_MEDIUM": {
"ScriptParamValue": 3,
"PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 1500
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsStreakHARD": {
"ScriptParamValue": 2,
"PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 3000,
"DuringSingleMatch": true
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsTargetInAirEASY": {
"ScriptParamValue": 1,
"PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 500
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsTargetInAirMEDIUM": {
"ScriptParamValue": 4,
"PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 1500
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsTargetInAirHARD": {
"ScriptParamValue": 3,
"PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 3000,
"DuringSingleMatch": true
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsWhileSlidingEASY": {
"ScriptParamValue": 1,
"PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 500
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsWhileSlidingMEDIUM": {
"ScriptParamValue": 4,
"PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 1500
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsWhileSlidingHARD": {
"ScriptParamValue": 3,
"PVPModeAllowed": ["PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 3000,
"DuringSingleMatch": true
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeMatchCompleteEASY": {
"ScriptParamValue": 1,
"PVPModeAllowed": ["PVPMODE_CAPTURETHEFLAG", "PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 500
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeMatchCompleteMEDIUM": {
"ScriptParamValue": 4,
"PVPModeAllowed": ["PVPMODE_CAPTURETHEFLAG", "PVPMODE_DEATHMATCH", "PVPMODE_TEAMDEATHMATCH"],
"SyndicateXP": 1500
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballCatchesEASY": {
"ScriptParamValue": 3,
"PVPModeAllowed": ["PVPMODE_SPEEDBALL"],
"SyndicateXP": 1000
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballCatchesMEDIUM": {
"ScriptParamValue": 10,
"PVPModeAllowed": ["PVPMODE_SPEEDBALL"],
"SyndicateXP": 3000
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballCatchesHARD": {
"ScriptParamValue": 6,
"PVPModeAllowed": ["PVPMODE_SPEEDBALL"],
"SyndicateXP": 6000,
"DuringSingleMatch": true
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballChecksEASY": {
"ScriptParamValue": 3,
"PVPModeAllowed": ["PVPMODE_SPEEDBALL"],
"SyndicateXP": 1000
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballChecksMEDIUM": {
"ScriptParamValue": 10,
"PVPModeAllowed": ["PVPMODE_SPEEDBALL"],
"SyndicateXP": 3000
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballChecksHARD": {
"ScriptParamValue": 6,
"PVPModeAllowed": ["PVPMODE_SPEEDBALL"],
"SyndicateXP": 6000,
"DuringSingleMatch": true
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballGoalsEASY": {
"ScriptParamValue": 2,
"PVPModeAllowed": ["PVPMODE_SPEEDBALL"],
"SyndicateXP": 1000
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballGoalsMEDIUM": {
"ScriptParamValue": 6,
"PVPModeAllowed": ["PVPMODE_SPEEDBALL"],
"SyndicateXP": 3000
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballGoalsHARD": {
"ScriptParamValue": 4,
"PVPModeAllowed": ["PVPMODE_SPEEDBALL"],
"SyndicateXP": 6000,
"DuringSingleMatch": true
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballInterceptionsEASY": {
"ScriptParamValue": 3,
"PVPModeAllowed": ["PVPMODE_SPEEDBALL"],
"SyndicateXP": 1000
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballInterceptionsMEDIUM": {
"ScriptParamValue": 6,
"PVPModeAllowed": ["PVPMODE_SPEEDBALL"],
"SyndicateXP": 3000
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballInterceptionsHARD": {
"ScriptParamValue": 6,
"PVPModeAllowed": ["PVPMODE_SPEEDBALL"],
"SyndicateXP": 6000,
"DuringSingleMatch": true
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballPassesEASY": {
"ScriptParamValue": 3,
"PVPModeAllowed": ["PVPMODE_SPEEDBALL"],
"SyndicateXP": 1000
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballPassesMEDIUM": {
"ScriptParamValue": 6,
"PVPModeAllowed": ["PVPMODE_SPEEDBALL"],
"SyndicateXP": 3000
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballPassesHARD": {
"ScriptParamValue": 3,
"PVPModeAllowed": ["PVPMODE_SPEEDBALL"],
"SyndicateXP": 6000,
"DuringSingleMatch": true
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballStealsEASY": {
"ScriptParamValue": 3,
"PVPModeAllowed": ["PVPMODE_SPEEDBALL"],
"SyndicateXP": 1000
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballStealsMEDIUM": {
"ScriptParamValue": 6,
"PVPModeAllowed": ["PVPMODE_SPEEDBALL"],
"SyndicateXP": 3000
},
"/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballStealsHARD": {
"ScriptParamValue": 3,
"PVPModeAllowed": ["PVPMODE_SPEEDBALL"],
"SyndicateXP": 6000
}
}

View File

@ -117,46 +117,6 @@
]
}
},
"Invasions": [
{
"_id": {
"$oid": "67c8ec8b3d0d86b236c1c18f"
},
"Faction": "FC_INFESTATION",
"DefenderFaction": "FC_CORPUS",
"Node": "SolNode53",
"Count": -28558,
"Goal": 30000,
"LocTag": "/Lotus/Language/Menu/InfestedInvasionBoss",
"Completed": false,
"ChainID": {
"$oid": "67c8b6a2bde0dfd0f7c1c18d"
},
"AttackerReward": [],
"AttackerMissionInfo": {
"seed": 488863,
"faction": "FC_CORPUS"
},
"DefenderReward": {
"countedItems": [
{
"ItemType": "/Lotus/Types/Items/Research/EnergyComponent",
"ItemCount": 3
}
]
},
"DefenderMissionInfo": {
"seed": 127653,
"faction": "FC_INFESTATION",
"missionReward": []
},
"Activation": {
"$date": {
"$numberLong": "1741221003031"
}
}
}
],
"SyndicateMissions": [
{
"_id": { "$oid": "663a4fc5ba6f84724fa4804c" },
@ -349,142 +309,8 @@
],
"PrimeAccessAvailability": { "State": "PRIME1" },
"PrimeVaultAvailabilities": [false, false, false, false, false],
"PrimeTokenAvailability": false,
"PrimeTokenAvailability": true,
"LibraryInfo": { "LastCompletedTargetType": "/Lotus/Types/Game/Library/Targets/Research7Target" },
"PVPChallengeInstances": [
{
"_id": { "$oid": "6635562d036ce37f7f98e264" },
"challengeTypeRefID": "/Lotus/PVPChallengeTypes/PVPTimedChallengeGameModeComplete",
"startDate": { "$date": { "$numberLong": "1714771501460" } },
"endDate": { "$date": { "$numberLong": "2000000000000" } },
"params": [{ "n": "ScriptParamValue", "v": 20 }],
"isGenerated": true,
"PVPMode": "PVPMODE_ALL",
"subChallenges": [],
"Category": "PVPChallengeTypeCategory_WEEKLY"
},
{
"_id": { "$oid": "6635562d036ce37f7f98e263" },
"challengeTypeRefID": "/Lotus/PVPChallengeTypes/PVPTimedChallengeGameModeWins",
"startDate": { "$date": { "$numberLong": "1714771501460" } },
"endDate": { "$date": { "$numberLong": "2000000000000" } },
"params": [{ "n": "ScriptParamValue", "v": 6 }],
"isGenerated": true,
"PVPMode": "PVPMODE_ALL",
"subChallenges": [],
"Category": "PVPChallengeTypeCategory_WEEKLY"
},
{
"_id": { "$oid": "6635562d036ce37f7f98e265" },
"challengeTypeRefID": "/Lotus/PVPChallengeTypes/PVPTimedChallengeOtherChallengeCompleteANY",
"startDate": { "$date": { "$numberLong": "1714771501460" } },
"endDate": { "$date": { "$numberLong": "2000000000000" } },
"params": [{ "n": "ScriptParamValue", "v": 10 }],
"isGenerated": true,
"PVPMode": "PVPMODE_ALL",
"subChallenges": [],
"Category": "PVPChallengeTypeCategory_WEEKLY"
},
{
"_id": { "$oid": "6635562d036ce37f7f98e266" },
"challengeTypeRefID": "/Lotus/PVPChallengeTypes/PVPTimedChallengeWeeklyStandardSet",
"startDate": { "$date": { "$numberLong": "1714771501460" } },
"endDate": { "$date": { "$numberLong": "2000000000000" } },
"params": [{ "n": "ScriptParamValue", "v": 0 }],
"isGenerated": true,
"PVPMode": "PVPMODE_NONE",
"subChallenges": [{ "$oid": "6635562d036ce37f7f98e263" }, { "$oid": "6635562d036ce37f7f98e264" }, { "$oid": "6635562d036ce37f7f98e265" }],
"Category": "PVPChallengeTypeCategory_WEEKLY_ROOT"
},
{
"_id": { "$oid": "6639ca6967c1192987d75fee" },
"challengeTypeRefID": "/Lotus/PVPChallengeTypes/PVPTimedChallengeFlagReturnEASY",
"startDate": { "$date": { "$numberLong": "1715063401824" } },
"endDate": { "$date": { "$numberLong": "2000000000000" } },
"params": [{ "n": "ScriptParamValue", "v": 1 }],
"isGenerated": true,
"PVPMode": "PVPMODE_CAPTURETHEFLAG",
"subChallenges": [],
"Category": "PVPChallengeTypeCategory_DAILY"
},
{
"_id": { "$oid": "6639ca6967c1192987d75fed" },
"challengeTypeRefID": "/Lotus/PVPChallengeTypes/PVPTimedChallengeMatchCompleteMEDIUM",
"startDate": { "$date": { "$numberLong": "1715063401824" } },
"endDate": { "$date": { "$numberLong": "2000000000000" } },
"params": [{ "n": "ScriptParamValue", "v": 4 }],
"isGenerated": true,
"PVPMode": "PVPMODE_CAPTURETHEFLAG",
"subChallenges": [],
"Category": "PVPChallengeTypeCategory_DAILY"
},
{
"_id": { "$oid": "6639ca6967c1192987d75ff2" },
"challengeTypeRefID": "/Lotus/PVPChallengeTypes/PVPTimedChallengeMatchCompleteEASY",
"startDate": { "$date": { "$numberLong": "1715063401824" } },
"endDate": { "$date": { "$numberLong": "2000000000000" } },
"params": [{ "n": "ScriptParamValue", "v": 1 }],
"isGenerated": true,
"PVPMode": "PVPMODE_DEATHMATCH",
"subChallenges": [],
"Category": "PVPChallengeTypeCategory_DAILY"
},
{
"_id": { "$oid": "6639ca6967c1192987d75ff1" },
"challengeTypeRefID": "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsPayback_MEDIUM",
"startDate": { "$date": { "$numberLong": "1715063401824" } },
"endDate": { "$date": { "$numberLong": "2000000000000" } },
"params": [{ "n": "ScriptParamValue", "v": 3 }],
"isGenerated": true,
"PVPMode": "PVPMODE_DEATHMATCH",
"subChallenges": [],
"Category": "PVPChallengeTypeCategory_DAILY"
},
{
"_id": { "$oid": "6639ca6967c1192987d75fef" },
"challengeTypeRefID": "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsStreakDominationEASY",
"startDate": { "$date": { "$numberLong": "1715063401824" } },
"endDate": { "$date": { "$numberLong": "2000000000000" } },
"params": [{ "n": "ScriptParamValue", "v": 1 }],
"isGenerated": true,
"PVPMode": "PVPMODE_TEAMDEATHMATCH",
"subChallenges": [],
"Category": "PVPChallengeTypeCategory_DAILY"
},
{
"_id": { "$oid": "6639ca6967c1192987d75ff0" },
"challengeTypeRefID": "/Lotus/PVPChallengeTypes/PVPTimedChallengeKillsWhileInAirHARD",
"startDate": { "$date": { "$numberLong": "1715063401824" } },
"endDate": { "$date": { "$numberLong": "2000000000000" } },
"params": [{ "n": "ScriptParamValue", "v": 3 }],
"isGenerated": true,
"PVPMode": "PVPMODE_TEAMDEATHMATCH",
"subChallenges": [],
"Category": "PVPChallengeTypeCategory_DAILY"
},
{
"_id": { "$oid": "6639ca6967c1192987d75ff3" },
"challengeTypeRefID": "/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballCatchesMEDIUM",
"startDate": { "$date": { "$numberLong": "1715063401824" } },
"endDate": { "$date": { "$numberLong": "2000000000000" } },
"params": [{ "n": "ScriptParamValue", "v": 10 }],
"isGenerated": true,
"PVPMode": "PVPMODE_SPEEDBALL",
"subChallenges": [],
"Category": "PVPChallengeTypeCategory_DAILY"
},
{
"_id": { "$oid": "6639ca6967c1192987d75ff4" },
"challengeTypeRefID": "/Lotus/PVPChallengeTypes/PVPTimedChallengeSpeedballInterceptionsEASY",
"startDate": { "$date": { "$numberLong": "1715063401824" } },
"endDate": { "$date": { "$numberLong": "2000000000000" } },
"params": [{ "n": "ScriptParamValue", "v": 3 }],
"isGenerated": true,
"PVPMode": "PVPMODE_SPEEDBALL",
"subChallenges": [],
"Category": "PVPChallengeTypeCategory_DAILY"
}
],
"PersistentEnemies": [],
"PVPAlternativeModes": [],
"PVPActiveTournaments": [],

View File

@ -388,11 +388,11 @@
<div class="card" style="height: 400px;">
<h5 class="card-header" data-loc="inventory_hoverboards"></h5>
<div class="card-body overflow-auto">
<form class="input-group mb-3" onsubmit="doAcquireModularEquipment('HoverBoards');return false;">
<input class="form-control" id="acquire-type-HoverBoards-HB_DECK" list="datalist-ModularParts-HB_DECK" />
<input class="form-control" id="acquire-type-HoverBoards-HB_ENGINE" list="datalist-ModularParts-HB_ENGINE" />
<input class="form-control" id="acquire-type-HoverBoards-HB_FRONT" list="datalist-ModularParts-HB_FRONT" />
<input class="form-control" id="acquire-type-HoverBoards-HB_JET" list="datalist-ModularParts-HB_JET" />
<form class="input-group mb-3" onsubmit="doAcquireModularEquipment('Hoverboards');return false;">
<input class="form-control" id="acquire-type-Hoverboards-HB_DECK" list="datalist-ModularParts-HB_DECK" />
<input class="form-control" id="acquire-type-Hoverboards-HB_ENGINE" list="datalist-ModularParts-HB_ENGINE" />
<input class="form-control" id="acquire-type-Hoverboards-HB_FRONT" list="datalist-ModularParts-HB_FRONT" />
<input class="form-control" id="acquire-type-Hoverboards-HB_JET" list="datalist-ModularParts-HB_JET" />
<button class="btn btn-primary" type="submit" data-loc="general_addButton"></button>
</form>
<table class="table table-hover w-100">
@ -436,29 +436,37 @@
<h5 class="card-header" data-loc="general_bulkActions"></h5>
<div class="card-body">
<div class="mb-2 d-flex flex-wrap gap-2">
<button class="btn btn-primary" onclick="addMissingEquipment(['Suits']);" data-loc="inventory_bulkAddSuits"></button>
<button class="btn btn-primary" onclick="addMissingEquipment(['Melee', 'LongGuns', 'Pistols']);" data-loc="inventory_bulkAddWeapons"></button>
<button class="btn btn-primary" onclick="addMissingEquipment(['SpaceSuits']);" data-loc="inventory_bulkAddSpaceSuits"></button>
<button class="btn btn-primary" onclick="addMissingEquipment(['SpaceGuns', 'SpaceMelee']);" data-loc="inventory_bulkAddSpaceWeapons"></button>
<button class="btn btn-primary" onclick="addMissingEquipment(['Sentinels']);" data-loc="inventory_bulkAddSentinels"></button>
<button class="btn btn-primary" onclick="addMissingEquipment(['SentinelWeapons']);" data-loc="inventory_bulkAddSentinelWeapons"></button>
<button class="btn btn-primary" onclick="addMissingEvolutionProgress();" data-loc="inventory_bulkAddEvolutionProgress"></button>
<button class="btn btn-primary" onclick="debounce(addMissingEquipment, ['Suits']);" data-loc="inventory_bulkAddSuits"></button>
<button class="btn btn-primary" onclick="debounce(addMissingEquipment, ['Melee', 'LongGuns', 'Pistols']);" data-loc="inventory_bulkAddWeapons"></button>
<button class="btn btn-primary" onclick="debounce(addMissingEquipment, ['SpaceSuits']);" data-loc="inventory_bulkAddSpaceSuits"></button>
<button class="btn btn-primary" onclick="debounce(addMissingEquipment, ['SpaceGuns', 'SpaceMelee']);" data-loc="inventory_bulkAddSpaceWeapons"></button>
<button class="btn btn-primary" onclick="debounce(addMissingEquipment, ['Sentinels']);" data-loc="inventory_bulkAddSentinels"></button>
<button class="btn btn-primary" onclick="debounce(addMissingEquipment, ['SentinelWeapons']);" data-loc="inventory_bulkAddSentinelWeapons"></button>
<button class="btn btn-primary" onclick="debounce(addMissingEvolutionProgress);" data-loc="inventory_bulkAddEvolutionProgress"></button>
</div>
<div class="mb-2 d-flex flex-wrap gap-2">
<button class="btn btn-success" onclick="maxRankAllEquipment(['Suits']);" data-loc="inventory_bulkRankUpSuits"></button>
<button class="btn btn-success" onclick="maxRankAllEquipment(['Melee', 'LongGuns', 'Pistols']);" data-loc="inventory_bulkRankUpWeapons"></button>
<button class="btn btn-success" onclick="maxRankAllEquipment(['SpaceSuits']);" data-loc="inventory_bulkRankUpSpaceSuits"></button>
<button class="btn btn-success" onclick="maxRankAllEquipment(['SpaceGuns', 'SpaceMelee']);" data-loc="inventory_bulkRankUpSpaceWeapons"></button>
<button class="btn btn-success" onclick="maxRankAllEquipment(['Sentinels']);" data-loc="inventory_bulkRankUpSentinels"></button>
<button class="btn btn-success" onclick="maxRankAllEquipment(['SentinelWeapons']);" data-loc="inventory_bulkRankUpSentinelWeapons"></button>
<button class="btn btn-success" onclick="maxRankAllEvolutions();" data-loc="inventory_bulkRankUpEvolutionProgress"></button>
<button class="btn btn-success" onclick="debounce(maxRankAllEquipment, ['Suits']);" data-loc="inventory_bulkRankUpSuits"></button>
<button class="btn btn-success" onclick="debounce(maxRankAllEquipment, ['Melee', 'LongGuns', 'Pistols']);" data-loc="inventory_bulkRankUpWeapons"></button>
<button class="btn btn-success" onclick="debounce(maxRankAllEquipment, ['SpaceSuits']);" data-loc="inventory_bulkRankUpSpaceSuits"></button>
<button class="btn btn-success" onclick="debounce(maxRankAllEquipment, ['SpaceGuns', 'SpaceMelee']);" data-loc="inventory_bulkRankUpSpaceWeapons"></button>
<button class="btn btn-success" onclick="debounce(maxRankAllEquipment, ['Sentinels']);" data-loc="inventory_bulkRankUpSentinels"></button>
<button class="btn btn-success" onclick="debounce(maxRankAllEquipment, ['SentinelWeapons']);" data-loc="inventory_bulkRankUpSentinelWeapons"></button>
<button class="btn btn-success" onclick="debounce(maxRankAllEvolutions);" data-loc="inventory_bulkRankUpEvolutionProgress"></button>
</div>
</div>
</div>
</div>
<div id="detailedView-route" data-route="/webui/detailedView" data-title="Inventory | OpenWF WebUI">
<h3 class="mb-0"></h3>
<h3 id="detailedView-loading" class="mb-0" data-loc="general_loading"></h3>
<h3 id="detailedView-title" class="mb-0"></h3>
<p class="text-body-secondary"></p>
<div id="loadout-card" class="card mb-3 d-none">
<h5 class="card-header" data-loc="detailedView_loadoutLabel"></h5>
<div class="card-body">
<ul class="nav nav-tabs" id="loadoutTabs"></ul>
<div class="tab-content mt-3" id="loadoutTabsContent"></div>
</div>
</div>
<div id="archonShards-card" class="card mb-3 d-none">
<h5 class="card-header" data-loc="detailedView_archonShardsLabel"></h5>
<div class="card-body">
@ -477,6 +485,62 @@
</table>
</div>
</div>
<div id="edit-suit-invigorations-card" class="card mb-3 d-none">
<h5 class="card-header" data-loc="detailedView_suitInvigorationLabel"></h5>
<div class="card-body">
<form onsubmit="submitSuitInvigorationUpgrade(event)">
<div class="mb-3">
<label for="invigoration-offensive" class="form-label" data-loc="invigorations_offensiveLabel"></label>
<select class="form-select" id="dv-invigoration-offensive">
<option value="" data-loc="general_none"></option>
<option value="/Lotus/Upgrades/Invigorations/Offensive/OffensiveInvigorationPowerStrength" data-loc="invigorations_offensive_AbilityStrength"></option>
<option value="/Lotus/Upgrades/Invigorations/Offensive/OffensiveInvigorationPowerRange" data-loc="invigorations_offensive_AbilityRange"></option>
<option value="/Lotus/Upgrades/Invigorations/Offensive/OffensiveInvigorationPowerDuration" data-loc="invigorations_offensive_AbilityDuration"></option>
<option value="/Lotus/Upgrades/Invigorations/Offensive/OffensiveInvigorationMeleeDamage" data-loc="invigorations_offensive_MeleeDamage"></option>
<option value="/Lotus/Upgrades/Invigorations/Offensive/OffensiveInvigorationPrimaryDamage" data-loc="invigorations_offensive_PrimaryDamage"></option>
<option value="/Lotus/Upgrades/Invigorations/Offensive/OffensiveInvigorationSecondaryDamage" data-loc="invigorations_offensive_SecondaryDamage"></option>
<option value="/Lotus/Upgrades/Invigorations/Offensive/OffensiveInvigorationPrimaryCritChance" data-loc="invigorations_offensive_PrimaryCritChance"></option>
<option value="/Lotus/Upgrades/Invigorations/Offensive/OffensiveInvigorationSecondaryCritChance" data-loc="invigorations_offensive_SecondaryCritChance"></option>
<option value="/Lotus/Upgrades/Invigorations/Offensive/OffensiveInvigorationMeleeCritChance" data-loc="invigorations_offensive_MeleeCritChance"></option>
</select>
</div>
<div class="mb-3">
<label for="invigoration-defensive" class="form-label" data-loc="invigorations_defensiveLabel"></label>
<select class="form-select" id="dv-invigoration-defensive">
<option value="" data-loc="general_none"></option>
<option value="/Lotus/Upgrades/Invigorations/Utility/UtilityInvigorationPowerEfficiency" data-loc="invigorations_utility_AbilityEfficiency"></option>
<option value="/Lotus/Upgrades/Invigorations/Utility/UtilityInvigorationMovementSpeed" data-loc="invigorations_utility_SprintSpeed"></option>
<option value="/Lotus/Upgrades/Invigorations/Utility/UtilityInvigorationParkourSpeed" data-loc="invigorations_utility_ParkourVelocity"></option>
<option value="/Lotus/Upgrades/Invigorations/Utility/UtilityInvigorationHealth" data-loc="invigorations_utility_HealthMax"></option>
<option value="/Lotus/Upgrades/Invigorations/Utility/UtilityInvigorationEnergy" data-loc="invigorations_utility_EnergyMax"></option>
<option value="/Lotus/Upgrades/Invigorations/Utility/UtilityInvigorationStatusResistance" data-loc="invigorations_utility_StatusImmune"></option>
<option value="/Lotus/Upgrades/Invigorations/Utility/UtilityInvigorationReloadSpeed" data-loc="invigorations_utility_ReloadSpeed"></option>
<option value="/Lotus/Upgrades/Invigorations/Utility/UtilityInvigorationHealthRegen" data-loc="invigorations_utility_HealthRegen"></option>
<option value="/Lotus/Upgrades/Invigorations/Utility/UtilityInvigorationArmor" data-loc="invigorations_utility_ArmorMax"></option>
<option value="/Lotus/Upgrades/Invigorations/Utility/UtilityInvigorationJumps" data-loc="invigorations_utility_Jumps"></option>
<option value="/Lotus/Upgrades/Invigorations/Utility/UtilityInvigorationEnergyRegen" data-loc="invigorations_utility_EnergyRegen"></option>
</select>
</div>
<div class="mb-3">
<label for="invigoration-expiry" class="form-label" data-loc="invigorations_expiryLabel"></label>
<input type="datetime-local" class="form-control" id="dv-invigoration-expiry" />
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary" data-loc="general_setButton"></button>
<button type="button" class="btn btn-danger" onclick="clearSuitInvigorationUpgrades()" data-loc="code_remove"></button>
</div>
</form>
</div>
</div>
<div id="modularParts-card" class="card mb-3 d-none">
<h5 class="card-header" data-loc="detailedView_modularPartsLabel"></h5>
<div class="card-body">
<form id="modularParts-form" class="input-group mb-3" onsubmit="handleModularPartsChange(event)"></form>
</div>
</div>
<div id="valenceBonus-card" class="card mb-3 d-none">
<h5 class="card-header" data-loc="detailedView_valenceBonusLabel"></h5>
<div class="card-body">
@ -485,7 +549,7 @@
<select class="form-control" id="valenceBonus-innateDamage"></select>
<input type="number" id="valenceBonus-procent" min="25" max="60" step="0.1" class="form-control" style="max-width:100px" />
<button class="btn btn-primary" type="submit" value="set" data-loc="general_setButton"></button>
<button class="btn btn-danger" type="submit" value="remove" data-loc="general_removeButton"></button>
<button class="btn btn-danger" type="submit" value="remove" data-loc="code_remove"></button>
</form>
</div>
</div>
@ -527,8 +591,8 @@
<form class="input-group mb-3" onsubmit="doAcquireMod();return false;">
<input class="form-control" id="mod-count" type="number" value="1"/>
<input class="form-control w-50" id="mod-to-acquire" list="datalist-mods" />
<button class="btn btn-success" onclick="window.maxed=true" type="submit" data-loc="mods_addMax"></button>
<button class="btn btn-primary" type="submit" data-loc="general_addButton"></button>
<button class="btn btn-success" onclick="window.maxed=true" data-loc="mods_addMax"></button>
</form>
<table class="table table-hover w-100">
<tbody id="mods-list"></tbody>
@ -599,26 +663,6 @@
<input class="form-check-input" type="checkbox" id="unlockAllScans" />
<label class="form-check-label" for="unlockAllScans" data-loc="cheats_unlockAllScans"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="infiniteCredits" />
<label class="form-check-label" for="infiniteCredits" data-loc="cheats_infiniteCredits"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="infinitePlatinum" />
<label class="form-check-label" for="infinitePlatinum" data-loc="cheats_infinitePlatinum"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="infiniteEndo" />
<label class="form-check-label" for="infiniteEndo" data-loc="cheats_infiniteEndo"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="infiniteRegalAya" />
<label class="form-check-label" for="infiniteRegalAya" data-loc="cheats_infiniteRegalAya"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="infiniteHelminthMaterials" />
<label class="form-check-label" for="infiniteHelminthMaterials" data-loc="cheats_infiniteHelminthMaterials"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="claimingBlueprintRefundsIngredients" />
<label class="form-check-label" for="claimingBlueprintRefundsIngredients" data-loc="cheats_claimingBlueprintRefundsIngredients"></label>
@ -803,21 +847,21 @@
<label class="form-label" for="spoofMasteryRank" data-loc="cheats_spoofMasteryRank"></label>
<div class="input-group">
<input class="form-control" id="spoofMasteryRank" type="number" min="-1" max="65535" data-default="-1" />
<button class="btn btn-primary" type="submit" data-loc="cheats_save"></button>
<button class="btn btn-secondary" type="submit" data-loc="cheats_save"></button>
</div>
</form>
<form class="form-group mt-2" onsubmit="doSaveConfigInt('relicRewardItemCountMultiplier'); return false;">
<label class="form-label" for="relicRewardItemCountMultiplier" data-loc="cheats_relicRewardItemCountMultiplier"></label>
<div class="input-group">
<input class="form-control" id="relicRewardItemCountMultiplier" type="number" min="1" max="1000000" data-default="1" />
<button class="btn btn-primary" type="submit" data-loc="cheats_save"></button>
<button class="btn btn-secondary" type="submit" data-loc="cheats_save"></button>
</div>
</form>
<form class="form-group mt-2" onsubmit="doSaveConfigInt('nightwaveStandingMultiplier'); return false;">
<label class="form-label" for="nightwaveStandingMultiplier" data-loc="cheats_nightwaveStandingMultiplier"></label>
<div class="input-group">
<input class="form-control" id="nightwaveStandingMultiplier" type="number" min="1" max="1000000" data-default="1" />
<button class="btn btn-primary" type="submit" data-loc="cheats_save"></button>
<button class="btn btn-secondary" type="submit" data-loc="cheats_save"></button>
</div>
</form>
</div>
@ -827,9 +871,30 @@
<div class="col-md-6">
<div class="card mb-3">
<h5 class="card-header" data-loc="cheats_account"></h5>
<div class="card-body">
<div class="mb-2 d-flex flex-wrap gap-2">
<div class="card-body" id="account-cheats">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="infiniteCredits" />
<label class="form-check-label" for="infiniteCredits" data-loc="cheats_infiniteCredits"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="infinitePlatinum" />
<label class="form-check-label" for="infinitePlatinum" data-loc="cheats_infinitePlatinum"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="infiniteEndo" />
<label class="form-check-label" for="infiniteEndo" data-loc="cheats_infiniteEndo"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="infiniteRegalAya" />
<label class="form-check-label" for="infiniteRegalAya" data-loc="cheats_infiniteRegalAya"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="infiniteHelminthMaterials" />
<label class="form-check-label" for="infiniteHelminthMaterials" data-loc="cheats_infiniteHelminthMaterials"></label>
</div>
<div class="mt-2 mb-2 d-flex flex-wrap gap-2">
<button class="btn btn-primary" onclick="debounce(doUnlockAllMissions);" data-loc="cheats_unlockAllMissions"></button>
<button class="btn btn-primary" onclick="debounce(markAllAsRead);" data-loc="cheats_markAllAsRead"></button>
<button class="btn btn-primary" onclick="doUnlockAllFocusSchools();" data-loc="cheats_unlockAllFocusSchools"></button>
<button class="btn btn-primary" onclick="doHelminthUnlockAll();" data-loc="cheats_helminthUnlockAll"></button>
<button class="btn btn-primary" onclick="debounce(addMissingHelminthRecipes);" data-loc="cheats_addMissingSubsumedAbilities"></button>
@ -861,22 +926,156 @@
<label class="form-check-label" for="worldState.resourceBoost" data-loc="worldState_resourceBoost"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="worldState.starDays" />
<label class="form-check-label" for="worldState.starDays" data-loc="worldState_starDays"></label>
<input class="form-check-input" type="checkbox" id="worldState.tennoLiveRelay" />
<label class="form-check-label" for="worldState.tennoLiveRelay" data-loc="worldState_tennoLiveRelay"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="worldState.baroTennoConRelay" />
<label class="form-check-label" for="worldState.baroTennoConRelay" data-loc="worldState_baroTennoConRelay"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="worldState.varziaFullyStocked" />
<label class="form-check-label" for="worldState.varziaFullyStocked" data-loc="worldState_varziaFullyStocked"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="worldState.wolfHunt" />
<label class="form-check-label" for="worldState.wolfHunt" data-loc="worldState_wolfHunt"></label>
<abbr data-loc-inc="worldState_galleonOfGhouls"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M320 576C461.4 576 576 461.4 576 320C576 178.6 461.4 64 320 64C178.6 64 64 178.6 64 320C64 461.4 178.6 576 320 576zM320 200C333.3 200 344 210.7 344 224L344 336C344 349.3 333.3 360 320 360C306.7 360 296 349.3 296 336L296 224C296 210.7 306.7 200 320 200zM293.3 416C292.7 406.1 297.6 396.7 306.1 391.5C314.6 386.4 325.3 386.4 333.8 391.5C342.3 396.7 347.2 406.1 346.6 416C347.2 425.9 342.3 435.3 333.8 440.5C325.3 445.6 314.6 445.6 306.1 440.5C297.6 435.3 292.7 425.9 293.3 416z"/></svg></abbr>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="worldState.longShadow" />
<label class="form-check-label" for="worldState.longShadow" data-loc="worldState_longShadow"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="worldState.hallowedFlame" />
<label class="form-check-label" for="worldState.hallowedFlame" data-loc="worldState_hallowedFlame"></label>
<abbr data-loc-inc="worldState_hallowedNightmares|worldState_dogDays"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M320 576C461.4 576 576 461.4 576 320C576 178.6 461.4 64 320 64C178.6 64 64 178.6 64 320C64 461.4 178.6 576 320 576zM320 200C333.3 200 344 210.7 344 224L344 336C344 349.3 333.3 360 320 360C306.7 360 296 349.3 296 336L296 224C296 210.7 306.7 200 320 200zM293.3 416C292.7 406.1 297.6 396.7 306.1 391.5C314.6 386.4 325.3 386.4 333.8 391.5C342.3 396.7 347.2 406.1 346.6 416C347.2 425.9 342.3 435.3 333.8 440.5C325.3 445.6 314.6 445.6 306.1 440.5C297.6 435.3 292.7 425.9 293.3 416z"/></svg></abbr>
</div>
<div class="form-group mt-2 d-flex gap-2">
<div class="flex-fill">
<label class="form-label" for="worldState.hallowedNightmares" data-loc="worldState_hallowedNightmares"></label>
<abbr data-loc-inc="worldState_hallowedFlame|worldState_dogDays"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M320 576C461.4 576 576 461.4 576 320C576 178.6 461.4 64 320 64C178.6 64 64 178.6 64 320C64 461.4 178.6 576 320 576zM320 200C333.3 200 344 210.7 344 224L344 336C344 349.3 333.3 360 320 360C306.7 360 296 349.3 296 336L296 224C296 210.7 306.7 200 320 200zM293.3 416C292.7 406.1 297.6 396.7 306.1 391.5C314.6 386.4 325.3 386.4 333.8 391.5C342.3 396.7 347.2 406.1 346.6 416C347.2 425.9 342.3 435.3 333.8 440.5C325.3 445.6 314.6 445.6 306.1 440.5C297.6 435.3 292.7 425.9 293.3 416z"/></svg></abbr>
<select class="form-control" id="worldState.hallowedNightmares" data-default="false">
<option value="true" data-loc="enabled"></option>
<option value="false" data-loc="disabled"></option>
</select>
</div>
<div class="flex-fill">
<label class="form-label" for="worldState.hallowedNightmaresRewardsOverride" data-loc="worldState_hallowedNightmaresRewards"></label>
<select class="form-control" id="worldState.hallowedNightmaresRewardsOverride" data-default="0">
<option value="0" data-loc="worldState_from_year" data-loc-year="2018"></option>
<option value="1" data-loc="worldState_from_year" data-loc-year="2016"></option>
<option value="2" data-loc="worldState_from_year" data-loc-year="2015"></option>
</select>
</div>
</div>
<div class="form-group mt-2 d-flex gap-2">
<div class="flex-fill">
<label class="form-label" for="worldState.proxyRebellion" data-loc="worldState_proxyRebellion"></label>
<select class="form-control" id="worldState.proxyRebellion" data-default="false">
<option value="true" data-loc="enabled"></option>
<option value="false" data-loc="disabled"></option>
</select>
</div>
<div class="flex-fill">
<label class="form-label" for="worldState.proxyRebellionRewardsOverride" data-loc="worldState_proxyRebellionRewards"></label>
<select class="form-control" id="worldState.proxyRebellionRewardsOverride" data-default="0">
<option value="0" data-loc="worldState_from_year" data-loc-year="2019"></option>
<option value="1" data-loc="worldState_from_year" data-loc-year="2018"></option>
</select>
</div>
</div>
<div class="form-group mt-2">
<label class="form-label" for="worldState.galleonOfGhouls" data-loc="worldState_galleonOfGhouls"></label>
<select class="form-control" id="worldState.galleonOfGhouls" data-default="">
<abbr data-loc-inc="worldState_wolfHunt"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M320 576C461.4 576 576 461.4 576 320C576 178.6 461.4 64 320 64C178.6 64 64 178.6 64 320C64 461.4 178.6 576 320 576zM320 200C333.3 200 344 210.7 344 224L344 336C344 349.3 333.3 360 320 360C306.7 360 296 349.3 296 336L296 224C296 210.7 306.7 200 320 200zM293.3 416C292.7 406.1 297.6 396.7 306.1 391.5C314.6 386.4 325.3 386.4 333.8 391.5C342.3 396.7 347.2 406.1 346.6 416C347.2 425.9 342.3 435.3 333.8 440.5C325.3 445.6 314.6 445.6 306.1 440.5C297.6 435.3 292.7 425.9 293.3 416z"/></svg></abbr>
<select class="form-control" id="worldState.galleonOfGhouls" data-default="0">
<option value="0" data-loc="disabled"></option>
<option value="1" data-loc="worldState_we1"></option>
<option value="2" data-loc="worldState_we2"></option>
<option value="3" data-loc="worldState_we3"></option>
</select>
</div>
<div class="form-group mt-2">
<label class="form-label" for="worldState.ghoulEmergenceOverride" data-loc="worldState_ghoulEmergence"></label>
<select class="form-control" id="worldState.ghoulEmergenceOverride" data-default="null">
<option value="null" data-loc="normal"></option>
<option value="true" data-loc="enabled"></option>
<option value="false" data-loc="disabled"></option>
</select>
</div>
<div class="form-group mt-2">
<label class="form-label" for="worldState.plagueStarOverride" data-loc="worldState_plagueStar"></label>
<select class="form-control" id="worldState.plagueStarOverride" data-default="null">
<option value="null" data-loc="normal"></option>
<option value="true" data-loc="enabled"></option>
<option value="false" data-loc="disabled"></option>
</select>
</div>
<div class="form-group mt-2">
<label class="form-label" for="worldState.starDaysOverride" data-loc="worldState_starDays"></label>
<select class="form-control" id="worldState.starDaysOverride" data-default="null">
<option value="null" data-loc="normal"></option>
<option value="true" data-loc="enabled"></option>
<option value="false" data-loc="disabled"></option>
</select>
</div>
<div class="form-group mt-2 d-flex gap-2">
<div class="flex-fill">
<label class="form-label" for="worldState.dogDaysOverride" data-loc="worldState_dogDays"></label>
<abbr data-loc-inc="worldState_hallowedFlame|worldState_hallowedNightmares"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M320 576C461.4 576 576 461.4 576 320C576 178.6 461.4 64 320 64C178.6 64 64 178.6 64 320C64 461.4 178.6 576 320 576zM320 200C333.3 200 344 210.7 344 224L344 336C344 349.3 333.3 360 320 360C306.7 360 296 349.3 296 336L296 224C296 210.7 306.7 200 320 200zM293.3 416C292.7 406.1 297.6 396.7 306.1 391.5C314.6 386.4 325.3 386.4 333.8 391.5C342.3 396.7 347.2 406.1 346.6 416C347.2 425.9 342.3 435.3 333.8 440.5C325.3 445.6 314.6 445.6 306.1 440.5C297.6 435.3 292.7 425.9 293.3 416z"/></svg></abbr>
<select class="form-control" id="worldState.dogDaysOverride" data-default="null">
<option value="null" data-loc="normal"></option>
<option value="true" data-loc="enabled"></option>
<option value="false" data-loc="disabled"></option>
</select>
</div>
<div class="flex-fill">
<label class="form-label" for="worldState.dogDaysRewardsOverride" data-loc="worldState_dogDaysRewards"></label>
<select class="form-control" id="worldState.dogDaysRewardsOverride" data-default="null">
<option value="null" data-loc="normal"></option>
<option value="3" data-loc="worldState_from_year" data-loc-year="2025"></option>
<option value="2" data-loc="worldState_from_year" data-loc-year="2024"></option>
<option value="1" data-loc="worldState_from_year" data-loc-year="2023"></option>
<option value="0" data-loc="worldState_pre_year" data-loc-year="2023"></option>
</select>
</div>
</div>
<div class="form-group mt-2 d-flex gap-2">
<div class="flex-fill">
<label class="form-label" for="worldState.bellyOfTheBeast" data-loc="worldState_bellyOfTheBeast"></label>
<select class="form-control" id="worldState.bellyOfTheBeast" data-default="false">
<option value="true" data-loc="enabled"></option>
<option value="false" data-loc="disabled"></option>
</select>
</div>
<div class="flex-fill">
<form class="form-group" onsubmit="doSaveConfigInt('worldState.bellyOfTheBeastProgressOverride'); return false;">
<label class="form-label" for="worldState.bellyOfTheBeastProgressOverride" data-loc="worldState_bellyOfTheBeastProgressOverride"></label>
<div class="input-group">
<input id="worldState.bellyOfTheBeastProgressOverride" class="form-control" type="number" min="0" max="100" data-default="0" />
<button class="btn btn-secondary" type="submit" data-loc="cheats_save"></button>
</div>
</form>
</div>
</div>
<div class="form-group mt-2 d-flex gap-2">
<div class="flex-fill">
<label class="form-label" for="worldState.eightClaw" data-loc="worldState_eightClaw"></label>
<select class="form-control" id="worldState.eightClaw" data-default="false">
<option value="true" data-loc="enabled"></option>
<option value="false" data-loc="disabled"></option>
</select>
</div>
<div class="flex-fill">
<form class="form-group" onsubmit="doSaveConfigInt('worldState.eightClawProgressOverride'); return false;">
<label class="form-label" for="worldState.eightClawProgressOverride" data-loc="worldState_eightClawProgressOverride"></label>
<div class="input-group">
<input id="worldState.eightClawProgressOverride" class="form-control" type="number" min="0" max="100" data-default="0" />
<button class="btn btn-secondary" type="submit" data-loc="cheats_save"></button>
</div>
</form>
</div>
</div>
<div class="form-group mt-2">
<label class="form-label" for="worldState.eidolonOverride" data-loc="worldState_eidolonOverride"></label>
<select class="form-control" id="worldState.eidolonOverride" data-default="">
@ -942,14 +1141,14 @@
<label class="form-label" for="worldState.circuitGameModes" data-loc="worldState_theCircuitOverride"></label>
<div class="input-group">
<input id="worldState.circuitGameModes" type="text" class="form-control tags-input" list="datalist-circuitGameModes" />
<button class="btn btn-primary" type="submit" data-loc="cheats_save"></button>
<button class="btn btn-secondary" type="submit" data-loc="cheats_save"></button>
</div>
</form>
<form class="form-group mt-2" onsubmit="doSaveConfigFloat('worldState.darvoStockMultiplier'); return false;">
<label class="form-label" for="worldState.darvoStockMultiplier" data-loc="worldState_darvoStockMultiplier"></label>
<div class="input-group">
<input id="worldState.darvoStockMultiplier" class="form-control" type="number" step="0.01" data-default="1" />
<button class="btn btn-primary" type="submit" data-loc="cheats_save"></button>
<button class="btn btn-secondary" type="submit" data-loc="cheats_save"></button>
</div>
</form>
</div>
@ -958,7 +1157,10 @@
</div>
</div>
<div data-route="/webui/import" data-title="Import | OpenWF WebUI">
<p data-loc="import_importNote"></p>
<p>
<span data-loc="import_importNote"></span>
<span data-loc="import_importNote2"></span>
</p>
<textarea class="form-control" id="import-inventory" style="height: calc(100vh - 300px)"></textarea>
<button class="btn btn-primary mt-3" onclick="doImport();" data-loc="import_submit"></button>
<p class="mt-3 mb-1" data-loc="import_samples"></p>
@ -1015,6 +1217,7 @@
<datalist id="datalist-ModularParts-KUBROW_ANTIGEN"></datalist>
<datalist id="datalist-ModularParts-KUBROW_MUTAGEN"></datalist>
<datalist id="datalist-Boosters"></datalist>
<datalist id="datalist-Abilities"></datalist>
<datalist id="datalist-circuitGameModes">
<option>Survival</option>
<option>VoidFlood</option>

View File

@ -18,7 +18,7 @@ const sendAuth = isRegister => {
window.ws.send(
JSON.stringify({
auth: {
email: localStorage.getItem("email"),
email: localStorage.getItem("email").toLowerCase(),
password: wp.encSync(localStorage.getItem("password")),
isRegister
}
@ -28,7 +28,8 @@ const sendAuth = isRegister => {
};
function openWebSocket() {
window.ws = new WebSocket("/custom/ws");
const wsProto = location.protocol === "https:" ? "wss://" : "ws://";
window.ws = new WebSocket(wsProto + location.host + "/custom/ws");
window.ws.onopen = () => {
ws_is_open = true;
sendAuth(false);
@ -118,9 +119,16 @@ function doLogin() {
window.registerSubmit = false;
}
async function revalidateAuthz() {
await getWebSocket();
// We have a websocket connection, so authz should be good.
function revalidateAuthz() {
return new Promise(resolve => {
let interval;
interval = setInterval(() => {
if (ws_is_open && !auth_pending) {
clearInterval(interval);
resolve();
}
}, 10);
});
}
function logout() {
@ -194,6 +202,17 @@ function updateLocElements() {
document.querySelectorAll("[data-loc-placeholder]").forEach(elm => {
elm.placeholder = loc(elm.getAttribute("data-loc-placeholder"));
});
document.querySelectorAll("[data-loc-inc]").forEach(elm => {
const incWith = elm
.getAttribute("data-loc-inc")
.split("|")
.map(key => loc(key))
.join(", ");
elm.title = `${loc("worldState_incompatibleWith")} ${incWith}`;
});
document.querySelectorAll("[data-loc-year]").forEach(elm => {
elm.innerHTML = elm.innerHTML.replace("|YEAR|", elm.getAttribute("data-loc-year"));
});
}
function setActiveLanguage(lang) {
@ -204,7 +223,7 @@ function setActiveLanguage(lang) {
document.querySelector("[data-lang=" + lang + "]").classList.add("active");
window.dictPromise = new Promise(resolve => {
const webui_lang = ["en", "ru", "fr", "de", "zh", "es"].indexOf(lang) == -1 ? "en" : lang;
const webui_lang = ["en", "ru", "fr", "de", "zh", "es", "uk"].indexOf(lang) == -1 ? "en" : lang;
let script = document.getElementById("translations");
if (script) document.documentElement.removeChild(script);
@ -273,6 +292,8 @@ function fetchItemList() {
window.itemListPromise = new Promise(resolve => {
const req = $.get("/custom/getItemLists?lang=" + window.lang);
req.done(async data => {
window.allQuestKeys = data.QuestKeys;
await dictPromise;
document.querySelectorAll('[id^="datalist-"]').forEach(datalist => {
@ -280,7 +301,8 @@ function fetchItemList() {
});
const syndicateNone = document.createElement("option");
syndicateNone.textContent = loc("cheats_none");
syndicateNone.value = "";
syndicateNone.textContent = loc("general_none");
document.getElementById("changeSyndicate").innerHTML = "";
document.getElementById("changeSyndicate").appendChild(syndicateNone);
@ -301,8 +323,8 @@ function fetchItemList() {
"/Lotus/Upgrades/Invigorations/ArchonCrystalUpgrades/ArchonCrystalUpgradeWarframeAbilityDurationMythic": loc("upgrade_WarframeAbilityDuration").split("|VAL|").join("15"),
"/Lotus/Upgrades/Invigorations/ArchonCrystalUpgrades/ArchonCrystalUpgradeWarframeAbilityStrength": loc("upgrade_WarframeAbilityStrength").split("|VAL|").join("10"),
"/Lotus/Upgrades/Invigorations/ArchonCrystalUpgrades/ArchonCrystalUpgradeWarframeAbilityStrengthMythic": loc("upgrade_WarframeAbilityStrength").split("|VAL|").join("15"),
"/Lotus/Upgrades/Invigorations/ArchonCrystalUpgrades/ArchonCrystalUpgradeWarframeArmourMax": loc("upgrade_WarframeArmourMax").split("|VAL|").join("150"),
"/Lotus/Upgrades/Invigorations/ArchonCrystalUpgrades/ArchonCrystalUpgradeWarframeArmourMaxMythic": loc("upgrade_WarframeArmourMax").split("|VAL|").join("225"),
"/Lotus/Upgrades/Invigorations/ArchonCrystalUpgrades/ArchonCrystalUpgradeWarframeArmourMax": loc("upgrade_WarframeArmorMax").split("|VAL|").join("150"),
"/Lotus/Upgrades/Invigorations/ArchonCrystalUpgrades/ArchonCrystalUpgradeWarframeArmourMaxMythic": loc("upgrade_WarframeArmorMax").split("|VAL|").join("225"),
"/Lotus/Upgrades/Invigorations/ArchonCrystalUpgrades/ArchonCrystalUpgradeWarframeBlastProc": loc("upgrade_WarframeBlastProc").split("|VAL|").join("5"),
"/Lotus/Upgrades/Invigorations/ArchonCrystalUpgrades/ArchonCrystalUpgradeWarframeBlastProcMythic": loc("upgrade_WarframeBlastProc").split("|VAL|").join("7.5"),
"/Lotus/Upgrades/Invigorations/ArchonCrystalUpgrades/ArchonCrystalUpgradeWarframeCastingSpeed": loc("upgrade_WarframeCastingSpeed").split("|VAL|").join("25"),
@ -331,8 +353,8 @@ function fetchItemList() {
"/Lotus/Upgrades/Invigorations/ArchonCrystalUpgrades/ArchonCrystalUpgradeWarframeParkourVelocityMythic": loc("upgrade_WarframeParkourVelocity").split("|VAL|").join("22.5"),
"/Lotus/Upgrades/Invigorations/ArchonCrystalUpgrades/ArchonCrystalUpgradeWarframeRadiationDamageBoost": loc("upgrade_WarframeRadiationDamageBoost").split("|VAL|").join("10"),
"/Lotus/Upgrades/Invigorations/ArchonCrystalUpgrades/ArchonCrystalUpgradeWarframeRadiationDamageBoostMythic": loc("upgrade_WarframeRadiationDamageBoost").split("|VAL|").join("15"),
"/Lotus/Upgrades/Invigorations/ArchonCrystalUpgrades/ArchonCrystalUpgradeWarframeRegen": loc("upgrade_WarframeRegen").split("|VAL|").join("5"),
"/Lotus/Upgrades/Invigorations/ArchonCrystalUpgrades/ArchonCrystalUpgradeWarframeRegenMythic": loc("upgrade_WarframeRegen").split("|VAL|").join("7.5"),
"/Lotus/Upgrades/Invigorations/ArchonCrystalUpgrades/ArchonCrystalUpgradeWarframeRegen": loc("upgrade_WarframeHealthRegen").split("|VAL|").join("5"),
"/Lotus/Upgrades/Invigorations/ArchonCrystalUpgrades/ArchonCrystalUpgradeWarframeRegenMythic": loc("upgrade_WarframeHealthRegen").split("|VAL|").join("7.5"),
"/Lotus/Upgrades/Invigorations/ArchonCrystalUpgrades/ArchonCrystalUpgradeWarframeShieldMax": loc("upgrade_WarframeShieldMax").split("|VAL|").join("150"),
"/Lotus/Upgrades/Invigorations/ArchonCrystalUpgrades/ArchonCrystalUpgradeWarframeShieldMaxMythic": loc("upgrade_WarframeShieldMax").split("|VAL|").join("225"),
"/Lotus/Upgrades/Invigorations/ArchonCrystalUpgrades/ArchonCrystalUpgradeWarframeStartingEnergy": loc("upgrade_WarframeStartingEnergy").split("|VAL|").join("30"),
@ -486,6 +508,9 @@ function fetchItemList() {
uniqueLevelCaps = items;
} else if (type == "Syndicates") {
items.forEach(item => {
if (item.uniqueName === "ConclaveSyndicate") {
return;
}
if (item.uniqueName.startsWith("RadioLegion")) {
item.name += " (" + item.uniqueName + ")";
}
@ -580,6 +605,8 @@ function fetchItemList() {
}
fetchItemList();
const accountCheats = document.querySelectorAll("#account-cheats input[id]");
// Assumes that caller revalidates authz
function updateInventory() {
const req = $.get("/api/inventory.php?" + window.authz + "&xpBasedLevelCapDisabled=1");
@ -728,7 +755,10 @@ function updateInventory() {
td.appendChild(a);
}
if (["Suits", "LongGuns", "Pistols", "Melee", "SpaceGuns", "SpaceMelee"].includes(category)) {
if (
["Suits", "LongGuns", "Pistols", "Melee", "SpaceGuns", "SpaceMelee"].includes(category) ||
modularWeapons.includes(item.ItemType)
) {
const a = document.createElement("a");
a.href = "/webui/detailedView?productCategory=" + category + "&itemId=" + item.ItemId.$oid;
a.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M278.5 215.6L23 471c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l57-57h68c49.7 0 97.9-14.4 139-41c11.1-7.2 5.5-23-7.8-23c-5.1 0-9.2-4.1-9.2-9.2c0-4.1 2.7-7.6 6.5-8.8l81-24.3c2.5-.8 4.8-2.1 6.7-4l22.4-22.4c10.1-10.1 2.9-27.3-11.3-27.3l-32.2 0c-5.1 0-9.2-4.1-9.2-9.2c0-4.1 2.7-7.6 6.5-8.8l112-33.6c4-1.2 7.4-3.9 9.3-7.7C506.4 207.6 512 184.1 512 160c0-41-16.3-80.3-45.3-109.3l-5.5-5.5C432.3 16.3 393 0 352 0s-80.3 16.3-109.3 45.3L139 149C91 197 64 262.1 64 330v55.3L253.6 195.8c6.2-6.2 16.4-6.2 22.6 0c5.4 5.4 6.1 13.6 2.2 19.8z"/></svg>`;
@ -859,15 +889,11 @@ function updateInventory() {
const datalistEvolutionProgress = document.querySelectorAll("#datalist-EvolutionProgress option");
const formEvolutionProgress = document.querySelector('form[onsubmit*="doAcquireEvolution()"]');
const giveAllQEvolutionProgress = document.querySelector(
'button[onclick*="addMissingEvolutionProgress()"]'
);
if (datalistEvolutionProgress.length === 0) {
formEvolutionProgress.classList.add("disabled");
formEvolutionProgress.querySelector("input").disabled = true;
formEvolutionProgress.querySelector("button").disabled = true;
giveAllQEvolutionProgress.disabled = true;
}
if (data.CrewShipHarnesses?.length) {
@ -879,6 +905,14 @@ function updateInventory() {
// Populate quests route
document.getElementById("QuestKeys-list").innerHTML = "";
window.allQuestKeys.forEach(questKey => {
if (!data.QuestKeys.some(x => x.ItemType == questKey.uniqueName)) {
const datalist = document.getElementById("datalist-QuestKeys");
if (!datalist.querySelector(`option[data-key="${questKey.uniqueName}"]`)) {
readdQuestKey(itemMap, questKey.uniqueName);
}
}
});
data.QuestKeys.forEach(item => {
const tr = document.createElement("tr");
tr.setAttribute("data-item-type", item.ItemType);
@ -972,10 +1006,7 @@ function updateInventory() {
a.href = "#";
a.onclick = function (event) {
event.preventDefault();
const option = document.createElement("option");
option.setAttribute("data-key", item.ItemType);
option.value = itemMap[item.ItemType]?.name ?? item.ItemType;
document.getElementById("datalist-QuestKeys").appendChild(option);
readdQuestKey(itemMap, item.ItemType);
doQuestUpdate("deleteKey", item.ItemType);
};
a.title = loc("code_remove");
@ -1010,13 +1041,19 @@ function updateInventory() {
if (item.ItemType.substr(0, 32) == "/Lotus/Upgrades/Mods/Randomized/") {
const rivenType = item.ItemType.substr(32);
const fingerprint = JSON.parse(item.UpgradeFingerprint);
if (fingerprint.buffs) {
if ("buffs" in fingerprint) {
// Riven has been revealed?
const tr = document.createElement("tr");
{
const td = document.createElement("td");
td.textContent = itemMap[fingerprint.compat]?.name ?? fingerprint.compat;
td.textContent += " " + RivenParser.parseRiven(rivenType, fingerprint, 1).name;
td.textContent += " ";
try {
td.textContent += RivenParser.parseRiven(rivenType, fingerprint, 1).name;
} catch (e) {
console.warn("malformed riven", { rivenType, fingerprint });
td.textContent += " [Malformed Riven]";
}
td.innerHTML +=
" <span title='" +
loc("code_buffsNumber") +
@ -1166,14 +1203,15 @@ function updateInventory() {
const item = data[category].find(x => x.ItemId.$oid == oid);
if (item) {
document.getElementById("detailedView-loading").classList.add("d-none");
if (item.ItemName) {
$("#detailedView-route h3").text(item.ItemName);
$("#detailedView-title").text(item.ItemName);
$("#detailedView-route .text-body-secondary").text(
itemMap[item.ItemType]?.name ?? item.ItemType
);
} else {
$("#detailedView-route h3").text(itemMap[item.ItemType]?.name ?? item.ItemType);
$("#detailedView-route .text-body-secondary").text("");
$("#detailedView-title").text(itemMap[item.ItemType]?.name ?? item.ItemType);
}
if (category == "Suits") {
@ -1213,6 +1251,127 @@ function updateInventory() {
}
document.getElementById("crystals-list").appendChild(tr);
});
document.getElementById("edit-suit-invigorations-card").classList.remove("d-none");
const { OffensiveUpgrade, DefensiveUpgrade, UpgradesExpiry } =
suitInvigorationUpgradeData(item);
document.getElementById("dv-invigoration-offensive").value = OffensiveUpgrade;
document.getElementById("dv-invigoration-defensive").value = DefensiveUpgrade;
document.getElementById("dv-invigoration-expiry").value = UpgradesExpiry;
{
document.getElementById("loadout-card").classList.remove("d-none");
const maxModConfigNum = Math.min(2 + (item.ModSlotPurchases ?? 0), 5);
const configs = item.Configs ?? [];
const loadoutTabs = document.getElementById("loadoutTabs");
const loadoutTabsContent = document.getElementById("loadoutTabsContent");
loadoutTabs.innerHTML = "";
loadoutTabsContent.innerHTML = "";
for (let i = 0; i <= maxModConfigNum; i++) {
const config = configs[i] ?? {};
{
const li = document.createElement("li");
li.classList.add("nav-item");
const button = document.createElement("button");
button.classList.add("nav-link");
if (i === 0) button.classList.add("active");
button.id = `config${i}-tab`;
button.setAttribute("data-bs-toggle", "tab");
button.setAttribute("data-bs-target", `#config${i}`);
button.innerHTML = config.Name?.trim() || String.fromCharCode(65 + i);
li.appendChild(button);
loadoutTabs.appendChild(li);
}
{
const tabDiv = document.createElement("div");
tabDiv.classList = "tab-pane";
if (i === 0) tabDiv.classList.add("show", "active");
tabDiv.id = `config${i}`;
{
const abilityOverrideForm = document.createElement("form");
abilityOverrideForm.classList = "form-group mt-2";
abilityOverrideForm.setAttribute(
"onsubmit",
`handleAbilityOverride(event, ${i});return false;`
);
const abilityOverrideFormLabel = document.createElement("label");
abilityOverrideFormLabel.setAttribute("data-loc", "abilityOverride_label");
abilityOverrideFormLabel.innerHTML = loc("abilityOverride_label");
abilityOverrideFormLabel.classList = "form-label";
abilityOverrideFormLabel.setAttribute(
"for",
`abilityOverride-ability-config-${i}`
);
abilityOverrideForm.appendChild(abilityOverrideFormLabel);
const abilityOverrideInputGroup = document.createElement("div");
abilityOverrideInputGroup.classList = "input-group";
abilityOverrideForm.appendChild(abilityOverrideInputGroup);
const abilityOverrideInput = document.createElement("input");
abilityOverrideInput.id = `abilityOverride-ability-config-${i}`;
abilityOverrideInput.classList = "form-control";
abilityOverrideInput.setAttribute("list", "datalist-Abilities");
if (config.AbilityOverride) {
const datalist = document.getElementById("datalist-Abilities");
const options = Array.from(datalist.options);
abilityOverrideInput.value = options.find(
option =>
config.AbilityOverride.Ability == option.getAttribute("data-key")
).value;
}
abilityOverrideInputGroup.appendChild(abilityOverrideInput);
const abilityOverrideOnSlot = document.createElement("span");
abilityOverrideOnSlot.classList = "input-group-text";
abilityOverrideOnSlot.setAttribute("data-loc", "abilityOverride_onSlot");
abilityOverrideOnSlot.innerHTML = loc("abilityOverride_onSlot");
abilityOverrideInputGroup.appendChild(abilityOverrideOnSlot);
const abilityOverrideSecondInput = document.createElement("input");
abilityOverrideSecondInput.id = `abilityOverride-ability-index-config-${i}`;
abilityOverrideSecondInput.classList = "form-control";
abilityOverrideSecondInput.setAttribute("type", "number");
abilityOverrideSecondInput.setAttribute("min", "0");
abilityOverrideSecondInput.setAttribute("max", "3");
if (config.AbilityOverride)
abilityOverrideSecondInput.value = config.AbilityOverride.Index;
abilityOverrideInputGroup.appendChild(abilityOverrideSecondInput);
const abilityOverrideSetButton = document.createElement("button");
abilityOverrideSetButton.classList = "btn btn-primary";
abilityOverrideSetButton.setAttribute("type", "submit");
abilityOverrideSetButton.setAttribute("value", "set");
abilityOverrideSetButton.setAttribute("data-loc", "general_setButton");
abilityOverrideSetButton.innerHTML = loc("general_setButton");
abilityOverrideInputGroup.appendChild(abilityOverrideSetButton);
const abilityOverrideRemoveButton = document.createElement("button");
abilityOverrideRemoveButton.classList = "btn btn-danger";
abilityOverrideRemoveButton.setAttribute("type", "submit");
abilityOverrideRemoveButton.setAttribute("value", "remove");
abilityOverrideRemoveButton.setAttribute("data-loc", "code_remove");
abilityOverrideRemoveButton.innerHTML = loc("code_remove");
abilityOverrideInputGroup.appendChild(abilityOverrideRemoveButton);
abilityOverrideForm.appendChild(abilityOverrideInputGroup);
tabDiv.appendChild(abilityOverrideForm);
}
loadoutTabsContent.appendChild(tabDiv);
}
}
}
} else if (["LongGuns", "Pistols", "Melee", "SpaceGuns", "SpaceMelee"].includes(category)) {
document.getElementById("valenceBonus-card").classList.remove("d-none");
document.getElementById("valenceBonus-innateDamage").value = "";
@ -1225,6 +1384,35 @@ function updateInventory() {
document.getElementById("valenceBonus-procent").value = Math.round(buffValue * 1000) / 10;
}
}
if (modularWeapons.includes(item.ItemType)) {
document.getElementById("modularParts-card").classList.remove("d-none");
const form = document.getElementById("modularParts-form");
form.innerHTML = "";
const requiredParts = getRequiredParts(category, item.ItemType);
requiredParts.forEach(modularPart => {
const input = document.createElement("input");
input.classList.add("form-control");
input.id = "detailedView-modularPart-" + modularPart;
input.setAttribute("list", "datalist-ModularParts-" + modularPart);
const datalist = document.getElementById("datalist-ModularParts-" + modularPart);
const options = Array.from(datalist.options);
input.value =
options.find(option => item.ModularParts.includes(option.getAttribute("data-key")))
?.value || "";
form.appendChild(input);
});
const changeButton = document.createElement("button");
changeButton.classList.add("btn");
changeButton.classList.add("btn-primary");
changeButton.type = "submit";
changeButton.setAttribute("data-loc", "cheats_changeButton");
changeButton.innerHTML = loc("cheats_changeButton");
form.appendChild(changeButton);
}
} else {
single.loadRoute("/webui/inventory");
}
@ -1287,6 +1475,10 @@ function updateInventory() {
}
document.getElementById("Boosters-list").appendChild(tr);
});
for (const elm of accountCheats) {
elm.checked = !!data[elm.id];
}
});
});
}
@ -1324,47 +1516,41 @@ function doAcquireEquipment(category) {
});
}
function doAcquireModularEquipment(category, WeaponType) {
let requiredParts;
let Parts = [];
function getRequiredParts(category, WeaponType) {
switch (category) {
case "HoverBoards":
WeaponType = "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit";
requiredParts = ["HB_DECK", "HB_ENGINE", "HB_FRONT", "HB_JET"];
break;
case "Hoverboards":
return ["HB_DECK", "HB_ENGINE", "HB_FRONT", "HB_JET"];
case "OperatorAmps":
requiredParts = ["AMP_OCULUS", "AMP_CORE", "AMP_BRACE"];
break;
return ["AMP_OCULUS", "AMP_CORE", "AMP_BRACE"];
case "Melee":
requiredParts = ["BLADE", "HILT", "HILT_WEIGHT"];
break;
return ["BLADE", "HILT", "HILT_WEIGHT"];
case "LongGuns":
requiredParts = ["GUN_BARREL", "GUN_PRIMARY_HANDLE", "GUN_CLIP"];
break;
return ["GUN_BARREL", "GUN_PRIMARY_HANDLE", "GUN_CLIP"];
case "Pistols":
requiredParts = ["GUN_BARREL", "GUN_SECONDARY_HANDLE", "GUN_CLIP"];
break;
return ["GUN_BARREL", "GUN_SECONDARY_HANDLE", "GUN_CLIP"];
case "MoaPets":
if (WeaponType == "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit") {
requiredParts = ["MOA_ENGINE", "MOA_PAYLOAD", "MOA_HEAD", "MOA_LEG"];
} else {
requiredParts = ["ZANUKA_BODY", "ZANUKA_HEAD", "ZANUKA_LEG", "ZANUKA_TAIL"];
}
break;
case "KubrowPets":
if (
[
"/Lotus/Types/Friendly/Pets/CreaturePets/VulpineInfestedCatbrowPetPowerSuit",
"/Lotus/Types/Friendly/Pets/CreaturePets/HornedInfestedCatbrowPetPowerSuit",
"/Lotus/Types/Friendly/Pets/CreaturePets/ArmoredInfestedCatbrowPetPowerSuit"
].includes(WeaponType)
) {
requiredParts = ["CATBROW_ANTIGEN", "CATBROW_MUTAGEN"];
} else {
requiredParts = ["KUBROW_ANTIGEN", "KUBROW_MUTAGEN"];
}
break;
return WeaponType === "/Lotus/Types/Friendly/Pets/MoaPets/MoaPetPowerSuit"
? ["MOA_ENGINE", "MOA_PAYLOAD", "MOA_HEAD", "MOA_LEG"]
: ["ZANUKA_BODY", "ZANUKA_HEAD", "ZANUKA_LEG", "ZANUKA_TAIL"];
case "KubrowPets": {
return WeaponType.endsWith("InfestedCatbrowPetPowerSuit")
? ["CATBROW_ANTIGEN", "CATBROW_MUTAGEN"]
: ["KUBROW_ANTIGEN", "KUBROW_MUTAGEN"];
}
}
}
function doAcquireModularEquipment(category, WeaponType) {
if (category === "Hoverboards") WeaponType = "/Lotus/Types/Vehicles/Hoverboard/HoverboardSuit";
const requiredParts = getRequiredParts(category, WeaponType);
let Parts = [];
requiredParts.forEach(part => {
const partName = getKey(document.getElementById("acquire-type-" + category + "-" + part));
if (partName) {
@ -1481,19 +1667,22 @@ function doAcquireEvolution() {
setEvolutionProgress([{ ItemType: uniqueName, Rank: permanentEvolutionWeapons.has(uniqueName) ? 0 : 1 }]);
}
$("input[list]").on("input", function () {
$(document).on("input", "input[list]", function () {
$(this).removeClass("is-invalid");
});
function dispatchAddItemsRequestsBatch(requests) {
revalidateAuthz().then(() => {
const req = $.post({
url: "/custom/addItems?" + window.authz,
contentType: "application/json",
data: JSON.stringify(requests)
});
req.done(() => {
updateInventory();
return new Promise(resolve => {
revalidateAuthz().then(() => {
const req = $.post({
url: "/custom/addItems?" + window.authz,
contentType: "application/json",
data: JSON.stringify(requests)
});
req.done(() => {
updateInventory();
resolve();
});
});
});
}
@ -1514,7 +1703,7 @@ function addMissingEquipment(categories) {
});
});
if (requests.length != 0 && window.confirm(loc("code_addItemsConfirm").split("|COUNT|").join(requests.length))) {
dispatchAddItemsRequestsBatch(requests);
return dispatchAddItemsRequestsBatch(requests);
}
}
@ -1530,7 +1719,7 @@ function addMissingEvolutionProgress() {
requests.push({ ItemType: uniqueName, Rank: permanentEvolutionWeapons.has(uniqueName) ? 0 : 1 });
});
if (requests.length != 0 && window.confirm(loc("code_addItemsConfirm").split("|COUNT|").join(requests.length))) {
setEvolutionProgress(requests);
return setEvolutionProgress(requests);
}
}
@ -1698,7 +1887,7 @@ function disposeOfGear(category, oid) {
];
revalidateAuthz().then(() => {
$.post({
url: "/api/sell.php?" + window.authz,
url: "/api/sell.php?" + window.authz + "&wsid=" + wsid,
contentType: "text/plain",
data: JSON.stringify(data)
});
@ -1720,7 +1909,7 @@ function disposeOfItems(category, type, count) {
];
revalidateAuthz().then(() => {
$.post({
url: "/api/sell.php?" + window.authz,
url: "/api/sell.php?" + window.authz + "&wsid=" + wsid,
contentType: "text/plain",
data: JSON.stringify(data)
});
@ -1757,14 +1946,17 @@ function maturePet(oid, revert) {
}
function setEvolutionProgress(requests) {
revalidateAuthz().then(() => {
const req = $.post({
url: "/custom/setEvolutionProgress?" + window.authz,
contentType: "application/json",
data: JSON.stringify(requests)
});
req.done(() => {
updateInventory();
return new Promise(resolve => {
revalidateAuthz().then(() => {
const req = $.post({
url: "/custom/setEvolutionProgress?" + window.authz,
contentType: "application/json",
data: JSON.stringify(requests)
});
req.done(() => {
updateInventory();
resolve();
});
});
});
}
@ -1923,6 +2115,8 @@ function doAcquireModMax() {
alert("doAcquireModMax: " + uniqueName);
}
// Cheats route
const uiConfigs = [...$(".config-form input[id], .config-form select[id]")].map(x => x.id);
for (const id of uiConfigs) {
@ -1930,7 +2124,13 @@ for (const id of uiConfigs) {
if (elm.tagName == "SELECT") {
elm.onchange = function () {
let value = this.value;
if (!isNaN(parseInt(value))) {
if (value == "true") {
value = true;
} else if (value == "false") {
value = false;
} else if (value == "null") {
value = null;
} else if (!isNaN(parseInt(value))) {
value = parseInt(value);
}
$.post({
@ -1954,6 +2154,19 @@ for (const id of uiConfigs) {
}
}
document.querySelectorAll(".config-form .input-group").forEach(grp => {
const input = grp.querySelector("input");
const btn = grp.querySelector("button");
input.oninput = input.onchange = function () {
btn.classList.remove("btn-secondary");
btn.classList.add("btn-primary");
};
btn.onclick = function () {
btn.classList.remove("btn-primary");
btn.classList.add("btn-secondary");
};
});
function doSaveConfigInt(id) {
$.post({
url: "/custom/setConfig?" + window.authz + "&wsid=" + wsid,
@ -1988,8 +2201,6 @@ function doSaveConfigStringArray(id) {
});
}
// Cheats route
single.getRoute("/webui/cheats").on("beforeload", function () {
let interval;
interval = setInterval(() => {
@ -2010,7 +2221,7 @@ single.getRoute("/webui/cheats").on("beforeload", function () {
if (elm.type == "checkbox") {
elm.checked = value;
} else if (elm.classList.contains("tags-input")) {
elm.value = value.join(", ");
elm.value = (value ?? []).join(", ");
elm.oninput();
} else {
elm.value = value ?? elm.getAttribute("data-default");
@ -2019,6 +2230,10 @@ single.getRoute("/webui/cheats").on("beforeload", function () {
})
.fail(res => {
if (res.responseText == "Log-in expired") {
if (ws_is_open && !auth_pending) {
console.warn("Credentials invalidated but the server didn't let us know");
sendAuth();
}
revalidateAuthz().then(() => {
if (single.getCurrentPath() == "/webui/cheats") {
single.loadRoute("/webui/cheats");
@ -2097,6 +2312,23 @@ function doIntrinsicsUnlockAll() {
});
}
document.querySelectorAll("#account-cheats input[type=checkbox]").forEach(elm => {
elm.onchange = function () {
revalidateAuthz().then(() => {
$.post({
url: "/custom/setAccountCheat?" + window.authz /*+ "&wsid=" + wsid*/,
contentType: "application/json",
data: JSON.stringify({
key: elm.id,
value: elm.checked
})
});
});
};
});
// Mods route
function doAddAllMods() {
let modsAll = new Set();
for (const child of document.getElementById("datalist-mods").children) {
@ -2141,7 +2373,7 @@ function doRemoveUnrankedMods() {
req.done(inventory => {
window.itemListPromise.then(itemMap => {
$.post({
url: "/api/sell.php?" + window.authz,
url: "/api/sell.php?" + window.authz + "&wsid=" + wsid,
contentType: "text/plain",
data: JSON.stringify({
SellCurrency: "SC_RegularCredits",
@ -2171,8 +2403,14 @@ function doAddMissingMaxRankMods() {
// DetailedView Route
single.getRoute("#detailedView-route").on("beforeload", function () {
this.element.querySelector("h3").textContent = "Loading...";
document.getElementById("detailedView-loading").classList.remove("d-none");
document.getElementById("detailedView-title").textContent = "";
document.querySelector("#detailedView-route .text-body-secondary").textContent = "";
document.getElementById("loadout-card").classList.add("d-none");
document.getElementById("archonShards-card").classList.add("d-none");
document.getElementById("edit-suit-invigorations-card").classList.add("d-none");
document.getElementById("modularParts-card").classList.add("d-none");
document.getElementById("modularParts-form").innerHTML = "";
document.getElementById("valenceBonus-card").classList.add("d-none");
if (window.didInitialInventoryUpdate) {
updateInventory();
@ -2254,6 +2492,13 @@ function doAddCurrency(currency) {
});
}
function readdQuestKey(itemMap, itemType) {
const option = document.createElement("option");
option.setAttribute("data-key", itemType);
option.value = itemMap[itemType]?.name ?? itemType;
document.getElementById("datalist-QuestKeys").appendChild(option);
}
function doQuestUpdate(operation, itemType) {
revalidateAuthz().then(() => {
$.post({
@ -2323,22 +2568,10 @@ function handleModularSelection(category) {
modularFieldsZanuka.style.display = "none";
}
} else if (inventoryCategory === "KubrowPets") {
if (
[
"/Lotus/Types/Friendly/Pets/CreaturePets/VulpineInfestedCatbrowPetPowerSuit",
"/Lotus/Types/Friendly/Pets/CreaturePets/HornedInfestedCatbrowPetPowerSuit",
"/Lotus/Types/Friendly/Pets/CreaturePets/ArmoredInfestedCatbrowPetPowerSuit"
].includes(key)
) {
if (key.endsWith("InfestedCatbrowPetPowerSuit")) {
modularFieldsCatbrow.style.display = "";
modularFieldsKubrow.style.display = "none";
} else if (
[
"/Lotus/Types/Friendly/Pets/CreaturePets/VizierPredatorKubrowPetPowerSuit",
"/Lotus/Types/Friendly/Pets/CreaturePets/PharaohPredatorKubrowPetPowerSuit",
"/Lotus/Types/Friendly/Pets/CreaturePets/MedjayPredatorKubrowPetPowerSuit"
].includes(key)
) {
} else if (key.endsWith("PredatorKubrowPetPowerSuit")) {
modularFieldsCatbrow.style.display = "none";
modularFieldsKubrow.style.display = "";
} else {
@ -2439,9 +2672,17 @@ function formatDatetime(fmt, date) {
const calls_in_flight = new Set();
async function debounce(func, ...args) {
calls_in_flight.add(func);
await func(...args);
calls_in_flight.delete(func);
if (!func.name) {
throw new Error(`cannot debounce anonymous functions`);
}
const callid = JSON.stringify({ func: func.name, args });
if (!calls_in_flight.has(callid)) {
calls_in_flight.add(callid);
await func(...args);
calls_in_flight.delete(callid);
} else {
console.log("debouncing", callid);
}
}
async function doMaxPlexus() {
@ -2764,3 +3005,146 @@ document.querySelectorAll("#sidebar .nav-link").forEach(function (elm) {
window.scrollTo(0, 0);
});
});
async function markAllAsRead() {
await revalidateAuthz();
const { Inbox } = await fetch("/api/inbox.php?" + window.authz).then(x => x.json());
let any = false;
for (const msg of Inbox) {
if (!msg.r) {
await fetch("/api/inbox.php?" + window.authz + "&messageId=" + msg.messageId.$oid);
any = true;
}
}
toast(loc(any ? "code_succRelog" : "code_nothingToDo"));
}
function handleModularPartsChange(event) {
event.preventDefault();
const urlParams = new URLSearchParams(window.location.search);
const form = document.getElementById("modularParts-form");
const inputs = form.querySelectorAll("input");
const modularParts = [];
inputs.forEach(input => {
const key = getKey(input);
if (!key) {
input.classList.add("is-invalid");
} else {
modularParts.push(key);
}
});
if (inputs.length == modularParts.length) {
revalidateAuthz().then(() => {
$.post({
url: "/custom/changeModularParts?" + window.authz,
contentType: "application/json",
data: JSON.stringify({
category: urlParams.get("productCategory"),
oid: urlParams.get("itemId"),
modularParts
})
}).then(function () {
toast(loc("code_succChange"));
updateInventory();
});
});
}
}
function suitInvigorationUpgradeData(suitData) {
let expiryDate = "";
if (suitData.UpgradesExpiry) {
if (suitData.UpgradesExpiry.$date) {
expiryDate = new Date(parseInt(suitData.UpgradesExpiry.$date.$numberLong));
} else if (typeof suitData.UpgradesExpiry === "number") {
expiryDate = new Date(suitData.UpgradesExpiry);
} else if (suitData.UpgradesExpiry instanceof Date) {
expiryDate = suitData.UpgradesExpiry;
}
if (expiryDate && !isNaN(expiryDate.getTime())) {
const year = expiryDate.getFullYear();
const month = String(expiryDate.getMonth() + 1).padStart(2, "0");
const day = String(expiryDate.getDate()).padStart(2, "0");
const hours = String(expiryDate.getHours()).padStart(2, "0");
const minutes = String(expiryDate.getMinutes()).padStart(2, "0");
expiryDate = `${year}-${month}-${day}T${hours}:${minutes}`;
} else {
expiryDate = "";
}
}
return {
oid: suitData.ItemId.$oid,
OffensiveUpgrade: suitData.OffensiveUpgrade || "",
DefensiveUpgrade: suitData.DefensiveUpgrade || "",
UpgradesExpiry: expiryDate
};
}
function submitSuitInvigorationUpgrade(event) {
event.preventDefault();
const oid = new URLSearchParams(window.location.search).get("itemId");
const offensiveUpgrade = document.getElementById("dv-invigoration-offensive").value;
const defensiveUpgrade = document.getElementById("dv-invigoration-defensive").value;
const expiry = document.getElementById("dv-invigoration-expiry").value;
if (!offensiveUpgrade || !defensiveUpgrade) {
alert(loc("code_requiredInvigorationUpgrade"));
return;
}
const data = {
OffensiveUpgrade: offensiveUpgrade,
DefensiveUpgrade: defensiveUpgrade
};
if (expiry) {
data.UpgradesExpiry = new Date(expiry).getTime();
}
editSuitInvigorationUpgrade(oid, data);
}
function clearSuitInvigorationUpgrades() {
editSuitInvigorationUpgrade(new URLSearchParams(window.location.search).get("itemId"), null);
}
async function editSuitInvigorationUpgrade(oid, data) {
/* data?: {
DefensiveUpgrade: string;
OffensiveUpgrade: string;
UpgradesExpiry?: number;
}*/
$.post({
url: "/custom/editSuitInvigorationUpgrade?" + window.authz,
contentType: "application/json",
data: JSON.stringify({ oid, data })
}).done(function () {
updateInventory();
});
}
function handleAbilityOverride(event, configIndex) {
event.preventDefault();
const urlParams = new URLSearchParams(window.location.search);
const action = event.submitter.value;
const Ability = getKey(document.getElementById(`abilityOverride-ability-config-${configIndex}`));
const Index = document.getElementById(`abilityOverride-ability-index-config-${configIndex}`).value;
revalidateAuthz().then(() => {
$.post({
url: "/custom/abilityOverride?" + window.authz,
contentType: "application/json",
data: JSON.stringify({
category: urlParams.get("productCategory"),
oid: urlParams.get("itemId"),
configIndex,
action,
AbilityOverride: {
Ability,
Index
}
})
}).done(function () {
updateInventory();
});
});
}

View File

@ -11,7 +11,8 @@
margin-left: 7rem;
}
body.logged-in:has([data-lang="de"].active) #main-content {
body.logged-in:has([data-lang="de"].active) #main-content,
body.logged-in:has([data-lang="uk"].active) #main-content {
margin-left: 8rem;
}
@ -28,9 +29,12 @@ body:not(.logged-in) .user-dropdown {
display: none;
}
td.text-end > a > svg {
/* font awesome icons */
svg {
fill: currentColor;
height: 1em;
}
td.text-end > a > svg {
margin-left: 0.5em;
margin-bottom: 4px; /* to centre the icon */
}

Some files were not shown because too many files have changed in this diff Show More