Compare commits

...

261 Commits

Author SHA1 Message Date
731be0d5e3 fix: exclude MT_ARENA from sortie node options (#1769)
Fixes #1764

Reviewed-on: OpenWF/SpaceNinjaServer#1769
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-21 22:06:39 -07:00
a49edefbd1 fix(webui): don't halve required R30 XP for MoaPets & KubrowPets (#1771)
Reviewed-on: OpenWF/SpaceNinjaServer#1771
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-21 15:21:40 -07:00
e3a34399e5 chore(webui): update to Spanish translation (#1772)
Reviewed-on: OpenWF/SpaceNinjaServer#1772
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-04-21 11:55:58 -07:00
ec6729db4d feat: setHubNpcCustomizations (#1762)
Closes #1757

Reviewed-on: OpenWF/SpaceNinjaServer#1762
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-21 10:44:12 -07:00
72b28f1d75 feat: send hex quest email when The Lotus Eaters and The Duviri Paradox are complete (#1761)
Close #1759

Reviewed-on: OpenWF/SpaceNinjaServer#1761
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-21 10:44:01 -07:00
bdf0ac722b feat: give lotus eaters quest at completion of whispers in the walls (#1760)
Closes #1758

Reviewed-on: OpenWF/SpaceNinjaServer#1760
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-21 10:43:26 -07:00
98aebba677 fix: EOM endo rewards showing as doubled in the client (#1756)
Closes #1754

Reviewed-on: OpenWF/SpaceNinjaServer#1756
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-21 10:43:10 -07:00
9912a623b1 fix: complete all quests not working (#1755)
Fixes #1742

Reviewed-on: OpenWF/SpaceNinjaServer#1755
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-21 10:42:56 -07:00
98975edca1 feat(webui): KubrowPets support (#1752)
also using `/api/modularWeaponCrafting.php` instead of  `/custom/addModularEquipment` for modular equipment

Reviewed-on: OpenWF/SpaceNinjaServer#1752
Reviewed-by: Sainan <sainan@calamity.inc>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-04-21 10:42:48 -07:00
218df461e1 feat: send WiTW email when completing The New War or Heart of Deimos (#1749)
At completion of either of the quests, check if the other has been completed, and if so, unlock WiTW.

Closes #1748

Reviewed-on: OpenWF/SpaceNinjaServer#1749
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-20 16:10:45 -07:00
86d871537b chore: add void corrupted moa to allScans.json 2025-04-20 17:18:00 +02:00
11f2ffe64d feat(import): accolades (#1750)
So one is able to import e.g. `{"Staff":true}` to set that field to true without going into Compass.

Reviewed-on: OpenWF/SpaceNinjaServer#1750
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-20 07:53:45 -07:00
8fd7152c41 fix: don't give rewards for aborted railjack missions (#1743)
Fixes #1741

Reviewed-on: OpenWF/SpaceNinjaServer#1743
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-20 07:53:11 -07:00
0f3d9f6c2c chore: provide upcoming weekly acts before week rollover (#1736)
The final piece to close #1640

Reviewed-on: OpenWF/SpaceNinjaServer#1736
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-19 09:06:49 -07:00
c2a633b549 chore: improve LiteSortie handling at week rollover (#1735)
WorldState now provides the upcoming LiteSortie if relevant and the boss is derived from the sortieId so completing it at rollover should work as expected.

Reviewed-on: OpenWF/SpaceNinjaServer#1735
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-19 09:06:38 -07:00
7040d422a2 feat: manage crew members (#1734)
Reviewed-on: OpenWF/SpaceNinjaServer#1734
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-19 09:06:20 -07:00
ba1380ec4c feat: rush repair drones (#1733)
Closes #1677

Reviewed-on: OpenWF/SpaceNinjaServer#1733
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-19 09:06:07 -07:00
26f37f58e5 chore(webui): make add mods behave more like adding items (#1732)
Reviewed-on: OpenWF/SpaceNinjaServer#1732
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-19 09:05:55 -07:00
e59bdcdfbc chore(webui): assume deleting items will always succeed (#1731)
instead of waiting for a response + then refreshing inventory, we can just delete the element right away and hope it works out

Reviewed-on: OpenWF/SpaceNinjaServer#1731
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-19 09:05:43 -07:00
c1ca303310 fix: handle mk1 armaments being salvaged (#1730)
Fixes #1729

Reviewed-on: OpenWF/SpaceNinjaServer#1730
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-19 09:05:23 -07:00
8afb515231 fix(stats): captures not being tracked for a new enemy (#1728)
Reviewed-on: OpenWF/SpaceNinjaServer#1728
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-19 09:04:22 -07:00
5eecf11b1a fix: ignore assassin mission failure if recovery is still pending (#1726)
Closes #1724

Reviewed-on: OpenWF/SpaceNinjaServer#1726
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-19 09:04:04 -07:00
37ac10acd2 chore: use import for static vendor manifest json files again (#1725)
This was changed because for VRST_WEAPON, the LocTagRandSeed is too big to be read without precision loss, but both vendors using it are now auto-generated, so we can have hot-reloading again when these files are changed.

Reviewed-on: OpenWF/SpaceNinjaServer#1725
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-19 09:03:53 -07:00
8b0ba0b84a feat: save InvasionProgress/QualifyingInvasions (#1719)
Reviewed-on: OpenWF/SpaceNinjaServer#1719
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-18 11:27:29 -07:00
cdead6fdf8 feat: archon hunt rewards (#1713)
also added a check for first completion to avoid giving another reward for repeating the final mission

Closes #1624

Reviewed-on: OpenWF/SpaceNinjaServer#1713
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-18 11:23:52 -07:00
da6067ec43 fix: use correct drop table for phorid assassination (#1718)
Reviewed-on: OpenWF/SpaceNinjaServer#1718
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-18 11:18:26 -07:00
a98e18d511 feat: tenet weapon vendor rotation (#1717)
Reviewed-on: OpenWF/SpaceNinjaServer#1717
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-18 11:18:11 -07:00
6394adb0f0 fix(webui): handle config get request failing due to expired authz (#1716)
Reviewed-on: OpenWF/SpaceNinjaServer#1716
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-18 11:17:55 -07:00
bc5dc02fc9 chore: fill in guild member data asynchronously (#1715)
Reviewed-on: OpenWF/SpaceNinjaServer#1715
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-18 11:17:36 -07:00
de5fd5fce0 chore: provide a proper schema for CurrentLoadOutIds (#1714)
Reviewed-on: OpenWF/SpaceNinjaServer#1714
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-18 11:17:19 -07:00
a6d4fab595 chore: rewrite gruzzling droptable to scathing/mocking whispers (#1712)
Closes #1708

Reviewed-on: OpenWF/SpaceNinjaServer#1712
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-18 11:16:58 -07:00
f549b042d6 feat: ignore list (#1711)
Closes #1707

Reviewed-on: OpenWF/SpaceNinjaServer#1711
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-18 11:16:43 -07:00
0c34c87d75 fix: give defaultUpgrades for infested pets (#1710)
Fixes #1709

Reviewed-on: OpenWF/SpaceNinjaServer#1710
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-18 11:16:11 -07:00
3baf6ad015 feat: handle railjack armaments, crew, & customizations in saveLoadout (#1706)
Closes #467

Reviewed-on: OpenWF/SpaceNinjaServer#1706
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-18 11:15:50 -07:00
196182f9a8 feat: acquisition of CrewMembers (#1705)
Reviewed-on: OpenWF/SpaceNinjaServer#1705
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-18 11:15:27 -07:00
379f57be2c chore: add pumpkin containers to allScans (#1703)
Closes #1693

Reviewed-on: OpenWF/SpaceNinjaServer#1703
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-17 18:40:51 -07:00
0d8f5ee66c fix: provide proper response when unbranding a suit (#1697)
Fixes #1695

Reviewed-on: OpenWF/SpaceNinjaServer#1697
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-17 18:40:41 -07:00
79492efbb4 chore: pass --enable-source-maps to node for npm run start (#1701)
Reviewed-on: OpenWF/SpaceNinjaServer#1701
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-17 13:15:50 -07:00
decbbdc81b chore(webui): update German translation (#1704)
Reviewed-on: OpenWF/SpaceNinjaServer#1704
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-04-17 12:17:15 -07:00
41d976d362 fix: don't trigger G3 capture when LevelKeyName is present (#1699)
Reviewed-on: OpenWF/SpaceNinjaServer#1699
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-17 10:58:40 -07:00
f94ecbfbfc chore: validate railjack repair start (#1698)
Reviewed-on: OpenWF/SpaceNinjaServer#1698
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-17 10:50:24 -07:00
f4f1e11b31 chore(webui): update Spanish translation (#1702)
Reviewed-on: OpenWF/SpaceNinjaServer#1702
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-04-17 10:46:22 -07:00
e38d52fb1b feat: sortie reward (#1692)
May work somewhat for lite sorties, didn't test that. They'd also need some extra handling with regards to the archon shards with their dynamic probabilities.

Reviewed-on: OpenWF/SpaceNinjaServer#1692
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-17 08:02:13 -07:00
419096f603 feat: noDeathMarks cheat (#1691)
Closes #1583

Reviewed-on: OpenWF/SpaceNinjaServer#1691
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-17 08:01:59 -07:00
76a53bb1f6 fix: don't consider simaris title 1 to earn a free favour (#1690)
Fixes #1688

Reviewed-on: OpenWF/SpaceNinjaServer#1690
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-17 08:00:31 -07:00
435aafeaae feat: randomly generate 1999 calendar seasons (#1689)
also handling week rollover now

Reviewed-on: OpenWF/SpaceNinjaServer#1689
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-17 08:00:19 -07:00
8a1603a661 feat: more comprehensive handling of railjack items in sellController (#1687)
Closes #1675

Reviewed-on: OpenWF/SpaceNinjaServer#1687
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-17 07:59:57 -07:00
66e34b7be9 feat: identify & repair railjack armaments (#1686)
Closes #1676

Reviewed-on: OpenWF/SpaceNinjaServer#1686
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-17 07:59:42 -07:00
9940024a01 fix: put acquired house version railjack armaments into raw salvage (#1685)
Reviewed-on: OpenWF/SpaceNinjaServer#1685
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-16 10:39:01 -07:00
ed217bae33 feat: cancel personal tech project (#1679)
Closes #1665

Reviewed-on: OpenWF/SpaceNinjaServer#1679
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-16 10:38:53 -07:00
16e850e7ee fix: provide a SubroutineIndex when identifying applicable components (#1683)
Reviewed-on: OpenWF/SpaceNinjaServer#1683
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-16 10:38:27 -07:00
850a073594 fix: don't set IsNew on CrewShipWeapons (#1682)
this indicator doesn't fully work for them as it seems the client doesn't clear it, so I assume they're not supposed to have it

Reviewed-on: OpenWF/SpaceNinjaServer#1682
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-16 10:37:40 -07:00
66dae6d3f8 chore(webui): update Spanish translation (#1681)
Reviewed-on: OpenWF/SpaceNinjaServer#1681
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-04-16 09:28:40 -07:00
9a50c05205 chore(webui): update to German translation (#1680)
Reviewed-on: OpenWF/SpaceNinjaServer#1680
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-04-16 09:01:47 -07:00
379e83a764 fix: use rewardTier only for rescue missions (#1674)
Fixes #1672

Reviewed-on: OpenWF/SpaceNinjaServer#1674
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-16 08:36:00 -07:00
44da0eb50a docs: some basic explanation of config.json & config.json.example 2025-04-16 15:35:39 +02:00
deb652ab37 fix: provide upcoming nightwave daily challenge if rollover is imminent (#1667)
Reviewed-on: OpenWF/SpaceNinjaServer#1667
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-16 06:31:16 -07:00
0ea67ea89a feat: identify & repair railjack components (#1664)
Closes #911

Reviewed-on: OpenWF/SpaceNinjaServer#1664
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-16 06:31:00 -07:00
51b82df5fd feat: granum void/purgatory rewards (#1663)
Closes #1627

Reviewed-on: OpenWF/SpaceNinjaServer#1663
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-16 06:30:36 -07:00
3d1b009bdb feat: noDailyFocusLimit cheat (#1661)
Closes #1641

Reviewed-on: OpenWF/SpaceNinjaServer#1661
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-16 06:30:22 -07:00
46aef2c00e feat: send jordas precept email when completing pluto to eris junction (#1660)
Closes #1659

Reviewed-on: OpenWF/SpaceNinjaServer#1660
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-16 06:30:06 -07:00
7d607b7348 fix: check ascension ceremony contributors when changing clan tier (#1656)
Reviewed-on: OpenWF/SpaceNinjaServer#1656
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-16 06:29:50 -07:00
4cb1ea94e5 feat: sell/scrap CrewShipWeapons (#1655)
Closes #1646

Reviewed-on: OpenWF/SpaceNinjaServer#1655
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-16 06:29:27 -07:00
729061951f fix: allow manageQuests' deleteKey op to be used with any ItemType (#1653)
Reviewed-on: OpenWF/SpaceNinjaServer#1653
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-16 06:29:09 -07:00
38502f10bf fix: give ample duplicates of ship decos with unlockAllShipDecorations (#1651)
Closes #1644

Reviewed-on: OpenWF/SpaceNinjaServer#1651
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-16 06:28:52 -07:00
a738dbfa9a fix: use JobTier instead of parsing the jobId for it (#1649)
Should fix #1647

Reviewed-on: OpenWF/SpaceNinjaServer#1649
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-16 06:28:34 -07:00
95562a97ad fix: provide current & upcoming sortie if rollover is imminent (#1666)
Reviewed-on: OpenWF/SpaceNinjaServer#1666
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-15 20:53:27 -07:00
c13615c4df fix: provide upcoming bounties in worldState when new cycle is imminent (#1657)
Reviewed-on: OpenWF/SpaceNinjaServer#1657
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-15 20:41:46 -07:00
eb6b1c1f57 chore: fix typo 2025-04-16 03:40:21 +02:00
64fbdf6064 fix: put house version railjack components into the salvage array (#1654)
Fixes #1645

Reviewed-on: OpenWF/SpaceNinjaServer#1654
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-15 18:11:13 -07:00
47551e93b3 feat(webui): add everything in ExportCustoms as an "add items" option 2025-04-16 03:11:21 +02:00
7a53363b1b fix response of giveQuestKeyReward 2025-04-16 01:36:46 +02:00
ea0ca8c88b chore: fix file name for giveQuestKeyRewardController 2025-04-16 01:35:28 +02:00
a10c3b061a fix: respect VaultsCracked when rolling droptable for level key rewards (#1639)
Fixes #1638

Reviewed-on: OpenWF/SpaceNinjaServer#1639
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-15 14:58:15 -07:00
3165d9f459 fix: respect rewardTier for rescue missions (#1650)
Reviewed-on: OpenWF/SpaceNinjaServer#1650
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-15 10:47:38 -07:00
28d7ca8ca0 chore: address eslint warnings 2025-04-15 18:48:17 +02:00
3f0a2bec48 fix: generate rewards based on RewardSeed to match what's show in client (#1628)
Reviewed-on: OpenWF/SpaceNinjaServer#1628
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-15 09:46:08 -07:00
d28437b658 feat: give 5 steel essence when completing an SP incursion (#1637)
Closes #1631

Reviewed-on: OpenWF/SpaceNinjaServer#1637
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-15 06:16:40 -07:00
a6d2c8b18a fix: don't give credits for junctions, the index, and free flight (#1635)
Closes #1625

Reviewed-on: OpenWF/SpaceNinjaServer#1635
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-15 06:16:31 -07:00
0c884576bd feat: picking up prex cards (#1634)
Closes #1621

Reviewed-on: OpenWF/SpaceNinjaServer#1634
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-15 06:16:19 -07:00
380f0662a4 fix: don't try to subtract MiscItems for polarity swap (#1633)
Closes #1620

Reviewed-on: OpenWF/SpaceNinjaServer#1633
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-15 06:16:07 -07:00
bd83738168 fix: provide a response to setPlacedDecoInfo (#1632)
This seems to be needed for the client when refreshing the ship after loading into a mission as it does not resync the ship otherwise.

Closes #1629

Reviewed-on: OpenWF/SpaceNinjaServer#1632
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-15 06:15:49 -07:00
fa68a1357d chore(webui): update to German translation (#1642)
Reviewed-on: OpenWF/SpaceNinjaServer#1642
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-04-15 06:15:33 -07:00
43f3917b09 fix: additional checks in bounty rewards (#1626)
Reviewed-on: OpenWF/SpaceNinjaServer#1626
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-04-15 06:10:25 -07:00
8ebb749732 chore(webui): update to Spanish translation (#1636)
Reviewed-on: OpenWF/SpaceNinjaServer#1636
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-04-14 11:45:00 -07:00
827ea47468 feat: personal quarters loadout, stencil, vignette, & fish customisation (#1619)
Closes #1618

Reviewed-on: OpenWF/SpaceNinjaServer#1619
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-14 07:16:25 -07:00
c64d466ce1 fix: universalPolarityEverywhere not applying on plexus aura slot (#1614)
Reviewed-on: OpenWF/SpaceNinjaServer#1614
Reviewed-by: Sainan <sainan@calamity.inc>
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-04-14 07:14:50 -07:00
c8ae3d688f feat: noResourceExtractorDronesDamage cheat (#1613)
Closes #1609

Reviewed-on: OpenWF/SpaceNinjaServer#1613
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-14 07:14:15 -07:00
4a971841a1 fix: check addItems quantity for Drones & EmailItems (#1612)
Closes #1610

Reviewed-on: OpenWF/SpaceNinjaServer#1612
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-14 07:13:51 -07:00
9472f855b6 chore: slightly generalise auto-generation of vendor manifests (#1611)
was gonna use this for the iron wake vendor manifest but the order is all wrong so in that way preprocessing remains a more preferable approach

Reviewed-on: OpenWF/SpaceNinjaServer#1611
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-14 07:13:36 -07:00
7736a2bf65 chore(webui): update to Spanish translation (#1616)
Reviewed-on: OpenWF/SpaceNinjaServer#1616
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-04-13 09:51:27 -07:00
bef3aeed72 chore(webui): update to German translation (#1615)
Reviewed-on: OpenWF/SpaceNinjaServer#1615
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-04-13 09:51:18 -07:00
aacd089123 feat: caliber chicks 2 rewards (#1606)
Closes #1263

Reviewed-on: OpenWF/SpaceNinjaServer#1606
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-13 05:52:21 -07:00
d281e929ae feat: noKimCooldowns cheat (#1605)
Closes #1537

Reviewed-on: OpenWF/SpaceNinjaServer#1605
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-13 05:51:54 -07:00
729ea0abff fix: look ahead for key chain messages (#1603)
This is required for the railjack quest:
- request has ChainStage 1 when it wants message from index 3
- request has ChainStage 4 when it wants message from index 6
- ...

Reviewed-on: OpenWF/SpaceNinjaServer#1603
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-13 05:51:27 -07:00
a75e0c59af feat: personal research (#1602)
This should be good enough for the railjack quest at least

Closes #1599

Reviewed-on: OpenWF/SpaceNinjaServer#1602
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-13 05:51:15 -07:00
b429eed46c feat: bounty item reward (#1595)
Re #388
same as before I think this only missing `Field Bounties` and `Arcana Isolation Vault`

Reviewed-on: OpenWF/SpaceNinjaServer#1595
Reviewed-by: Sainan <sainan@calamity.inc>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-04-13 05:51:02 -07:00
92d2616dda feat: handle duet encounter (#1592)
Closes #1274

Reviewed-on: OpenWF/SpaceNinjaServer#1592
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-13 05:50:51 -07:00
37ccd33d5c chore(webui): update to German translation (#1607)
Reviewed-on: OpenWF/SpaceNinjaServer#1607
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-04-13 05:48:38 -07:00
20326fdaa0 chore(webui): strip <SHARD_BLUE_SIMPLE> etc from item name 2025-04-13 05:08:14 +02:00
6a97a0c7c8 chore: default ChallengesFixVersion to 6 (#1594)
we don't set this field anywhere but it might be needed

Reviewed-on: OpenWF/SpaceNinjaServer#1594
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-12 18:55:01 -07:00
0928b842ad fix: handle acquisition of weapon slots via nightwave (#1591)
Reviewed-on: OpenWF/SpaceNinjaServer#1591
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-12 18:54:49 -07:00
e0200b2111 fix: universalPolarityEverywhere not affecting all plexus slots (#1589)
Reviewed-on: OpenWF/SpaceNinjaServer#1589
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-12 18:54:42 -07:00
18a13911ba fix: handle content-encoding "e" (#1588)
Reviewed-on: OpenWF/SpaceNinjaServer#1588
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-12 18:53:35 -07:00
2c53d17489 chore: update pe+ (#1604)
Reviewed-on: OpenWF/SpaceNinjaServer#1604
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-12 18:52:01 -07:00
2eb28c4e89 chore: fix unnecessary condition 2025-04-12 23:59:06 +02:00
2187d9cd7e chore(webui): note performance impact of archon shards 2025-04-12 23:56:33 +02:00
e5e6f7963b chore(webui): update ru translation (#1598)
Reviewed-on: OpenWF/SpaceNinjaServer#1598
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-04-12 14:46:24 -07:00
900c6e9a26 chore: enforce UNIX-style line endings 2025-04-12 23:45:26 +02:00
f0ee1e8aad feat(webui): Spanish translation 2025-04-12 23:37:08 +02:00
5c6b4b5779 fix: set activation & expiry for eidolon/venus/deimos bounties (#1581)
Reviewed-on: OpenWF/SpaceNinjaServer#1581
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-12 06:15:14 -07:00
9b330ffd3e feat: sendMsgToInBox (#1580)
Reviewed-on: OpenWF/SpaceNinjaServer#1580
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-12 06:15:03 -07:00
97d27e8110 feat: playedParkourTutorial (#1579)
Reviewed-on: OpenWF/SpaceNinjaServer#1579
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-12 06:14:46 -07:00
525e3067c9 fix: only warn when addKeyChainItems does not change the inventory (#1578)
Reviewed-on: OpenWF/SpaceNinjaServer#1578
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-12 06:14:21 -07:00
0c1fa05e9c chore: don't error on setDojoURL (#1571)
users may be confused about the "unknown endpoint" message, as it is reported with error level

Reviewed-on: OpenWF/SpaceNinjaServer#1571
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-12 06:14:02 -07:00
946f3129b8 feat: bounty standing reward (#1556)
Re #388
I think this only missing `Field Bounties` and `Arcana Isolation Vault`

Reviewed-on: OpenWF/SpaceNinjaServer#1556
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-04-12 06:13:44 -07:00
355de3fa04 chore(webui): update to German translation (#1575)
Reviewed-on: OpenWF/SpaceNinjaServer#1575
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-04-11 08:51:39 -07:00
61e168e444 chore: add Conselor to IInventoryClient
this isn't an accolade, but it unlocks a new chat
2025-04-11 16:55:23 +02:00
70fa48ab07 chore(webui): update to German translation (#1568)
Reviewed-on: OpenWF/SpaceNinjaServer#1568
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-04-11 06:57:35 -07:00
dde95c2b61 feat: favoriting equipment & skins (#1555)
Reviewed-on: OpenWF/SpaceNinjaServer#1555
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-11 06:56:45 -07:00
2ca79ef898 feat: eidolon/venus/deimos bounty rotation (#1554)
Reviewed-on: OpenWF/SpaceNinjaServer#1554
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-11 06:56:31 -07:00
63e3c96671 feat: transmutation of requiem/antivirus/potency mods (#1553)
Reviewed-on: OpenWF/SpaceNinjaServer#1553
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-11 06:56:12 -07:00
f0351489be feat: HeistProfitTakerBountyThree first time completion reward (#1552)
Reviewed-on: OpenWF/SpaceNinjaServer#1552
Reviewed-by: Sainan <sainan@calamity.inc>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-04-11 06:54:59 -07:00
5149d0e382 chore: don't use ? for leaderboardService parameters (#1551)
This means the argument can be omitted when we really mean that it can be undefined — but we still want it to be explicitly given.

Reviewed-on: OpenWF/SpaceNinjaServer#1551
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-11 06:54:47 -07:00
ec8982a921 feat: bounty rewards (#1549)
Re #388. This only handles reward manifests and only those given in the worldState.

Reviewed-on: OpenWF/SpaceNinjaServer#1549
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-11 06:54:35 -07:00
cc338c2173 feat: cheat Unlock All Dojo Deco Recipes (#1543)
Reviewed-on: OpenWF/SpaceNinjaServer#1543
Reviewed-by: Sainan <sainan@calamity.inc>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-04-11 06:54:07 -07:00
85b5bb438e feat: handle OtherDialogueInfos in saveDialogue (#1542)
Reviewed-on: OpenWF/SpaceNinjaServer#1542
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-11 06:53:50 -07:00
9f727789ca chore: split worldState stuff into types & service (#1548)
Reviewed-on: OpenWF/SpaceNinjaServer#1548
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-10 12:54:48 -07:00
b308b91f44 chore: remove typescript version limit (#1547)
This is no longer needed now that the eslint stuff is up-to-date enough.

Reviewed-on: OpenWF/SpaceNinjaServer#1547
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-10 12:54:43 -07:00
fc3ef3a126 fix: use wagerTier for The Index rewards (#1545)
Reviewed-on: OpenWF/SpaceNinjaServer#1545
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-10 12:54:29 -07:00
3f47f89b56 chore: update PE+ (#1546)
and make use of some of the new data

Reviewed-on: OpenWF/SpaceNinjaServer#1546
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-10 12:40:57 -07:00
e784b2dfb8 chore: fix typo 2025-04-10 21:05:35 +02:00
c0947b8822 chore(webui): use select for "supported syndicate" (#1539)
Reviewed-on: OpenWF/SpaceNinjaServer#1539
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-10 07:17:40 -07:00
a0b61bec12 feat: KIM gifts (#1538)
Reviewed-on: OpenWF/SpaceNinjaServer#1538
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-10 07:16:54 -07:00
d3620c00e2 feat: automatically delete death mark messages after 24 hours (#1535)
Possibly unfaithful but more faithful than never deleting it at all.

Reviewed-on: OpenWF/SpaceNinjaServer#1535
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-10 07:16:30 -07:00
0ffcee5faf fix: set deathmark message title to the boss' name (#1533)
Reviewed-on: OpenWF/SpaceNinjaServer#1533
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-10 07:16:06 -07:00
c2ed8b40f0 feat: track EudicoHeists in CompletedJobChains (#1531)
Reviewed-on: OpenWF/SpaceNinjaServer#1531
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-10 07:15:54 -07:00
feb1dd4715 chore: improve changeDojoRoot (#1522)
Using SortId instead of actually changing the component ids.
What's strange is that providing/omitting SortId does seem to make a difference in regards to deco positioning, which is presumably what the POST body would be for. I've opted to simply always provide the SortId in hopes that this avoids the need for repositioning entirely.
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-10 07:15:27 -07:00
540961ff9e chore(webui): use gildWeaponController (#1518)
also use `TEquipmentKey` instead `WeaponTypeInternal | "Hoverboards"`
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-04-10 07:14:53 -07:00
5692a6201e feat: No Dojo Deco Build Stage cheat (#1508)
Reviewed-on: OpenWF/SpaceNinjaServer#1508
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-04-10 07:14:33 -07:00
541b8d32a8 feat: LizzieShawzin (#1525)
Reviewed-on: OpenWF/SpaceNinjaServer#1525
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-09 15:37:28 -07:00
74f9d1567f feat: handle QueuedDialogues in saveDialogue (#1524)
Reviewed-on: OpenWF/SpaceNinjaServer#1524
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-09 15:30:59 -07:00
02a4d2b30a feat: track KIM resets (#1528)
This was added in 38.5.0 for FlareRank1Convo3

Reviewed-on: OpenWF/SpaceNinjaServer#1528
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-09 15:28:29 -07:00
a8f174bce1 fix: don't duplicate FlavourItems (#1526)
Reviewed-on: OpenWF/SpaceNinjaServer#1526
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-09 15:26:40 -07:00
db1dd21924 ci: improve prettier coverage (#1523)
All prettier violations will now be reported, not just what eslint checks.

Reviewed-on: OpenWF/SpaceNinjaServer#1523
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-09 15:25:19 -07:00
005350bde0 ci: remove node.js version matrix (#1519)
We only check if the TypeScript successfully compiles & lints, which isn't really dependent on Node.js version.

Reviewed-on: OpenWF/SpaceNinjaServer#1519
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-09 06:52:30 -07:00
5cd18db7a7 chore: prettier 2025-04-09 13:30:04 +02:00
bb315eaafe chore: handle addItem of crew ship harness (#1516)
Reviewed-on: OpenWF/SpaceNinjaServer#1516
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-09 03:48:07 -07:00
327b834b07 chore: handle zealoid prelate stripped rewards (#1515)
Reviewed-on: OpenWF/SpaceNinjaServer#1515
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-08 03:06:47 -07:00
39be095818 chore: handle season challenge completion in missionInventoryUpdate (#1511)
Reviewed-on: OpenWF/SpaceNinjaServer#1511
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-08 03:06:36 -07:00
ef4973e694 chrore(webui): don't add duplicates to datalists (#1510)
Browsers will show all options, even if this makes no sense, causing some confusion for users.

Reviewed-on: OpenWF/SpaceNinjaServer#1510
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-08 03:06:19 -07:00
7f69667171 feat: dojo component settings (#1509)
Reviewed-on: OpenWF/SpaceNinjaServer#1509
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-08 03:06:06 -07:00
dcdeb0cd34 feat(webui): add pigment (#1507)
Reviewed-on: OpenWF/SpaceNinjaServer#1507
Reviewed-by: Sainan <sainan@calamity.inc>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-04-08 03:05:53 -07:00
8ce86ad4fd chore(webui): show quantity for recipes (#1506)
Reviewed-on: OpenWF/SpaceNinjaServer#1506
Reviewed-by: Sainan <sainan@calamity.inc>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-04-07 05:55:33 -07:00
a2f1469779 feat: add attVisualOnly to inbox messages (#1499)
In case we'll need it...

Reviewed-on: OpenWF/SpaceNinjaServer#1499
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-07 05:30:29 -07:00
dd32e082f3 chore: add UmbraDate to equipment (#1496)
Reviewed-on: OpenWF/SpaceNinjaServer#1496
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-07 05:30:15 -07:00
74c7d86090 feat: polychrome (#1495)
Reviewed-on: OpenWF/SpaceNinjaServer#1495
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-07 05:30:00 -07:00
7fd4d50e07 feat(webui): add level keys via "add items" (#1493)
Reviewed-on: OpenWF/SpaceNinjaServer#1493
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-07 05:29:44 -07:00
7f805a1dcc feat: handle KeyToRemove in EOM upload (#1491)
Reviewed-on: OpenWF/SpaceNinjaServer#1491
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-07 05:29:32 -07:00
919f12b8f9 feat: sortie rotation (#1453)
Reviewed-on: OpenWF/SpaceNinjaServer#1453
Reviewed-by: Sainan <sainan@calamity.inc>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-04-07 05:29:21 -07:00
94993a16aa fix: use await instead of void 2025-04-07 01:13:47 +02:00
002b0cb93f chore: fix code duplication for quest completion (#1497)
Reviewed-on: OpenWF/SpaceNinjaServer#1497
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-06 16:08:36 -07:00
4362a842ff Fix the rewards of Second Dream (#1498)
Adds the two missing inbox items of Second Dream.

Reviewed-on: OpenWF/SpaceNinjaServer#1498
Co-authored-by: VampireKitten <dynamightkobold@gmail.com>
Co-committed-by: VampireKitten <dynamightkobold@gmail.com>
2025-04-06 16:05:47 -07:00
5702ab5f3b fix: missing AutoContributeFromVault in guild response 2025-04-06 23:21:43 +02:00
fac52bfda1 fix: scale credits subtracted from clan vault when auto-contributing 2025-04-06 23:19:00 +02:00
2ff535e7ab chore: update PE+ 2025-04-06 21:20:00 +02:00
9698baa979 feat: handle droptable rewards from level key (#1492)
Reviewed-on: OpenWF/SpaceNinjaServer#1492
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-06 10:19:15 -07:00
ceb7deec06 chore: generate source maps with build (#1489)
This ensures that when we get a stack trace, it contains the original line numbers.

Reviewed-on: OpenWF/SpaceNinjaServer#1489
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-06 10:18:50 -07:00
fe0b745066 fix: missing fields in dojo response (#1488)
Reviewed-on: OpenWF/SpaceNinjaServer#1488
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-06 10:18:33 -07:00
8f41d3c13f fix: give an extra trade when leveling up MR (#1487)
Reviewed-on: OpenWF/SpaceNinjaServer#1487
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-06 10:18:15 -07:00
f906cdb5e8 fix: handle client providing an invalid loadout id at EOM upload (#1486)
Reviewed-on: OpenWF/SpaceNinjaServer#1486
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-06 10:18:01 -07:00
ea6facf3fc chore(webui): update to German translation (#1490)
Reviewed-on: OpenWF/SpaceNinjaServer#1490
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-04-06 06:44:11 -07:00
b93a4a6dae fix: handle login reward not being able to give any recipe (#1479)
Reviewed-on: OpenWF/SpaceNinjaServer#1479
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-06 06:04:55 -07:00
64da8c2e50 feat: no mastery rank up cooldown cheat (#1478)
Reviewed-on: OpenWF/SpaceNinjaServer#1478
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-06 06:04:44 -07:00
b2497ded19 fix: refuse to add items non-fatally (#1476)
This is needed to complete to the railjack quest when already owning a railjack

Reviewed-on: OpenWF/SpaceNinjaServer#1476
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-06 06:04:30 -07:00
5f6b2330af chore: remove /Lotus/Types/Recipes/ from path-based logic (#1475)
Both recipes & MiscItems (recipe components) start with this.

Reviewed-on: OpenWF/SpaceNinjaServer#1475
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-06 06:04:16 -07:00
2bdb722986 fix: invalid format in inventory response for UpgradesExpiry (#1473)
Reviewed-on: OpenWF/SpaceNinjaServer#1473
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-06 06:04:04 -07:00
3c79f910a2 feat: coda weapon vendor rotation (#1471)
Reviewed-on: OpenWF/SpaceNinjaServer#1471
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-06 06:03:55 -07:00
65306e0478 chore(webui): update to German translation (#1469)
Reviewed-on: OpenWF/SpaceNinjaServer#1469
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-04-05 07:45:10 -07:00
49edebc1eb chore: fix controllers exporting non-RequestHandler things (#1468)
I'm surprised JavaScript allows circular includes, but they still don't feel good.

Reviewed-on: OpenWF/SpaceNinjaServer#1468
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-05 06:52:35 -07:00
62263efde3 chore: simplify serversideVendorsService's api (#1467)
Reviewed-on: OpenWF/SpaceNinjaServer#1467
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-05 06:52:13 -07:00
f66c958a3c feat: change alliance member permissions (#1466)
Reviewed-on: OpenWF/SpaceNinjaServer#1466
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-05 06:51:54 -07:00
2ef59cd570 chore: split confirmGuildInvitation get & post controllers (#1465)
Reviewed-on: OpenWF/SpaceNinjaServer#1465
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-05 06:51:37 -07:00
6bb74b026a feat: contribute to allied clan vault (#1462)
Reviewed-on: OpenWF/SpaceNinjaServer#1462
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-05 06:51:23 -07:00
743a905de4 fix: ignore non-weapon entries in ExportWeapons for recipe login reward (#1461)
Reviewed-on: OpenWF/SpaceNinjaServer#1461
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-05 06:51:10 -07:00
5c22949c6b chore: improve handling when config.json is missing & fix logger options (#1460)
Reviewed-on: OpenWF/SpaceNinjaServer#1460
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-05 06:50:57 -07:00
651640c4d7 chore(vscode): recommend eslint extension 2025-04-05 02:56:29 +02:00
1d1abf5550 chore: remove unused eslint-disable directives 2025-04-05 02:54:06 +02:00
23267aa641 feat: leave alliance/kick alliance members (#1459)
Reviewed-on: OpenWF/SpaceNinjaServer#1459
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-04 15:18:10 -07:00
d94b4fd946 chore: use parallelForeach in deleteGuild (#1458)
Reviewed-on: OpenWF/SpaceNinjaServer#1458
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-04 15:16:57 -07:00
d5ff349746 fix: update TradesRemaining at daily reset (#1457)
Reviewed-on: OpenWF/SpaceNinjaServer#1457
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-04 15:16:46 -07:00
2746e243c9 chore: add async-utils 2025-04-04 15:12:25 +02:00
b3374eb66e feat: divvy alliance vault (#1455)
Reviewed-on: OpenWF/SpaceNinjaServer#1455
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-04 06:03:12 -07:00
c18abab9c4 feat: handle miscItemFee in end of match upload (#1454)
Reviewed-on: OpenWF/SpaceNinjaServer#1454
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-04 06:02:55 -07:00
61062e433f feat: personal decos in dojo & move dojo decos (#1451)
Reviewed-on: OpenWF/SpaceNinjaServer#1451
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-04 06:02:40 -07:00
92e8ffd709 feat: alliance invites (#1452)
Reviewed-on: OpenWF/SpaceNinjaServer#1452
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-03 19:04:21 -07:00
0b18932dd8 chore: remove duplicate conditional 2025-04-04 02:46:32 +02:00
abeb17ce44 chore: add alliance information to getAccountInfo (#1439)
Reviewed-on: OpenWF/SpaceNinjaServer#1439
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-03 10:41:01 -07:00
710470ca2d feat(webui): quests support (#1411)
Reviewed-on: OpenWF/SpaceNinjaServer#1411
Reviewed-by: Sainan <sainan@calamity.inc>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-04-03 10:40:22 -07:00
5cc991baca fix: reduce DailyFocus by earned focus XP (#1448)
Reviewed-on: OpenWF/SpaceNinjaServer#1448
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-03 10:40:02 -07:00
9eadc7fa21 feat: auto-contribute from clan vault (#1435)
The wiki says this is also supposed to do partial fills, but didn't see that in my testing on live.

Reviewed-on: OpenWF/SpaceNinjaServer#1435
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-03 10:39:16 -07:00
ed10a89c1d feat: alliance motd (#1438)
Reviewed-on: OpenWF/SpaceNinjaServer#1438
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-03 10:38:37 -07:00
0c2f72f9b1 fix: don't charge platinum for renaming kaithe (#1440)
Reviewed-on: OpenWF/SpaceNinjaServer#1440
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-03 10:38:23 -07:00
d918b0c982 fix: don't remove consumed argon crystals from FoundToday (#1447)
This fixes a possible mongo conflict when ticking them, and this is probably more desirable as you wanna consume unstable crystals first.

Reviewed-on: OpenWF/SpaceNinjaServer#1447
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-03 10:38:11 -07:00
05c0c9909c fix: ignore purchaseQuantity when giving mission rewards (#1446)
Reviewed-on: OpenWF/SpaceNinjaServer#1446
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-03 10:37:52 -07:00
92cf85084f chore: remove needless query when sending clan invite (#1434)
Reviewed-on: OpenWF/SpaceNinjaServer#1434
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-03 06:17:38 -07:00
cfa9ec775e feat: handle creditsFee in missionInventoryUpdate (#1431)
Reviewed-on: OpenWF/SpaceNinjaServer#1431
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-03 06:17:11 -07:00
d4d887a5a4 chore: prettier 2025-04-03 00:34:26 +02:00
1b7b5a28bc chore: limit number of kubrow eggs that can be acquired at once 2025-04-02 22:33:49 +02:00
6dc54ed893 feat: donate credits to alliance vault (#1436)
Reviewed-on: OpenWF/SpaceNinjaServer#1436
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-02 13:26:44 -07:00
2173bdb8b8 fix: clamp ItemCount within 32-bit integer range (#1432)
It seems the game uses 32-bit ints for these values on the C++ side before passing them on to Lua where they become floats. This would cause the game to have overflow/underflow semantics when receiving values outside of these bounds.

Reviewed-on: OpenWF/SpaceNinjaServer#1432
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-02 13:20:06 -07:00
74d9428a66 fix(webui): ignore MiscItems that don't have a name (#1429)
Reviewed-on: OpenWF/SpaceNinjaServer#1429
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-02 09:52:24 -07:00
dd7805cfb2 chore: fix some minor issues with ability infusions (#1426)
Reviewed-on: OpenWF/SpaceNinjaServer#1426
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-02 09:52:13 -07:00
c55aa8a0e1 chore: fix misplaced index 2025-04-02 18:51:15 +02:00
24ed580a97 feat: create alliance (#1423)
Reviewed-on: OpenWF/SpaceNinjaServer#1423
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-02 04:59:21 -07:00
158310bda2 fix(webui): blacklist modular weapons from add missing (#1425)
Reviewed-on: OpenWF/SpaceNinjaServer#1425
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-04-02 04:43:03 -07:00
2b451a19e6 chore: remove duplicate entries (#1424)
CrewShipWeapons and CrewShipSalvagedWeapons already in equipmentFields

Reviewed-on: OpenWF/SpaceNinjaServer#1424
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-04-02 04:42:36 -07:00
bf67a4391d feat: eleanor weapon offerings (#1419)
Need to do rotating offers for her some other time

Reviewed-on: OpenWF/SpaceNinjaServer#1419
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-01 15:49:08 -07:00
ea9333279b fix: handle CurrentLoadOutIds missing LoadOuts in missionInventoryUpdate (#1421)
Reviewed-on: OpenWF/SpaceNinjaServer#1421
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-01 15:48:40 -07:00
9e1a5d50af chore: slightly more faithful cutoff for valence fusion (#1418)
Reviewed-on: OpenWF/SpaceNinjaServer#1418
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-01 02:29:51 -07:00
2091dabfc3 chore: use tsgo to verify types (#1417)
Reviewed-on: OpenWF/SpaceNinjaServer#1417
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-01 02:29:41 -07:00
3a26d788a2 feat: zanuka capture (#1416)
Reviewed-on: OpenWF/SpaceNinjaServer#1416
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-01 02:29:29 -07:00
367dd3f22d feat: consign pet (#1415)
Reviewed-on: OpenWF/SpaceNinjaServer#1415
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-01 02:29:05 -07:00
404c747642 feat: getProfileViewingData for clans (#1412)
Reviewed-on: OpenWF/SpaceNinjaServer#1412
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-01 02:28:48 -07:00
1a4ad8b7a5 feat: clan applications (#1410)
Reviewed-on: OpenWF/SpaceNinjaServer#1410
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-01 02:28:24 -07:00
4a3a3de300 chore: fix eslint warning 2025-04-01 02:28:52 +02:00
7d5ea680e4 chore(webui): remove "<ARCHWING> " from item name (#1414)
Reviewed-on: OpenWF/SpaceNinjaServer#1414
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-03-31 13:59:18 -07:00
e2879a7808 chore(webui): update translations (#1413)
translations were taken from the game

Reviewed-on: OpenWF/SpaceNinjaServer#1413
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-03-31 13:54:55 -07:00
d033c2bc12 feat(webui): MoaPets support (#1402)
Translations were taken from the game

Reviewed-on: OpenWF/SpaceNinjaServer#1402
Reviewed-by: Sainan <sainan@calamity.inc>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-03-31 09:18:41 -07:00
fb58aeb07f chore: reimplement setWeaponSkillTree as a mongo query (#1406)
This is faster by like 1-2 ms

Reviewed-on: OpenWF/SpaceNinjaServer#1406
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-03-31 09:18:33 -07:00
3d69828610 fix: give non-exalted additional items when acquiring warframe (#1408)
Also upgraded `no-misused-promises` to an error and added `IsNew` field to powersuits.

Reviewed-on: OpenWF/SpaceNinjaServer#1408
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-03-31 09:18:25 -07:00
a0fa41cd58 chore: accept ObjectId for accountId when sending inbox messages (#1409)
Reviewed-on: OpenWF/SpaceNinjaServer#1409
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-03-31 09:18:00 -07:00
9162522962 chore: update PE+ (#1407)
Reviewed-on: OpenWF/SpaceNinjaServer#1407
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-03-31 04:51:12 -07:00
42e08faaaf chore: handle account switching guilds (#1398)
Plus some additional inventory cleanup when a guild is being deleted forcefully.
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-03-31 04:26:55 -07:00
9e0dd3e0a5 chore: run save operations in parallel where possible (#1401)
Reviewed-on: OpenWF/SpaceNinjaServer#1401
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-03-31 04:26:44 -07:00
054abee62c chore: use inventory projection in sellController (#1399)
Yeah, it's not pretty but it's a good amount faster.

Reviewed-on: OpenWF/SpaceNinjaServer#1399
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-03-31 04:16:09 -07:00
04d39ed973 chore: use SubdocumentArray.id in some more places (#1400)
Reviewed-on: OpenWF/SpaceNinjaServer#1400
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-03-31 04:15:32 -07:00
b0f0b61d49 fix: allow completion of unknown nodes (#1395)
Reviewed-on: OpenWF/SpaceNinjaServer#1395
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-03-31 04:15:00 -07:00
23f8901505 fix: reduce platinum cost of rushing recipes based on progress (#1393)
Reviewed-on: OpenWF/SpaceNinjaServer#1393
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-03-31 04:14:35 -07:00
d3d966a503 feat: grustrag bolt (#1392)
Reviewed-on: OpenWF/SpaceNinjaServer#1392
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-03-31 04:14:20 -07:00
48598c145f feat: guild ads (#1390)
Reviewed-on: OpenWF/SpaceNinjaServer#1390
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-03-31 04:14:00 -07:00
01f04c287a fix: add RemovedIdItems to valence fusion response (#1397)
Reviewed-on: OpenWF/SpaceNinjaServer#1397
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-03-30 13:50:59 -07:00
9e99d0370c fix: align dojo component DestructionTime to full seconds (#1394)
not doing this causes the client to spam requests and have some UI bugs

Reviewed-on: OpenWF/SpaceNinjaServer#1394
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-03-30 13:50:36 -07:00
516f822e43 feat: clan tiers (#1378)
Reviewed-on: OpenWF/SpaceNinjaServer#1378
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-03-30 09:58:51 -07:00
fccdbf4a8e fix: detect kuva weapons more reliably (#1388)
it seems not all of them have the InnateDamageRandomMod or even VT_KUVA so just assuming that any weapon with max rank 40 that's not the ballas sword needs it
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-03-30 09:58:44 -07:00
cfc1524619 fix: give quest completion items from cheated completion too (#1376)
Reviewed-on: OpenWF/SpaceNinjaServer#1376
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-03-30 08:13:24 -07:00
3beb1ecc42 chore: use ExportKeys for quests not in questCompletionItems (#1377)
Reviewed-on: OpenWF/SpaceNinjaServer#1377
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-03-30 08:13:11 -07:00
779bc34082 feat(webui): adding kitgun (#1382)
Reviewed-on: OpenWF/SpaceNinjaServer#1382
Reviewed-by: Sainan <sainan@calamity.inc>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-03-30 08:12:46 -07:00
b6167165fe chore(webui): put all ModularParts in itemLists (#1383)
Reviewed-on: OpenWF/SpaceNinjaServer#1383
Reviewed-by: Sainan <sainan@calamity.inc>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-03-30 08:12:31 -07:00
b07e89ed72 chore(webui): update to German translation (#1386)
Reviewed-on: OpenWF/SpaceNinjaServer#1386
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-03-30 07:13:48 -07:00
f34e1615e2 fix(webui): show 0 rerolls instead NaN in Rivens (#1385)
Co-authored-by: Sainan <sainan@calamity.inc>
Reviewed-on: OpenWF/SpaceNinjaServer#1385
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-03-30 06:54:58 -07:00
c82cad7b02 chore(webui): update ru loc (#1384)
Reviewed-on: OpenWF/SpaceNinjaServer#1384
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-03-30 06:39:32 -07:00
725efcc72e chore: replace copyfiles with ncp (#1381)
They're both unmaintained, but this one is smaller at least.

Reviewed-on: OpenWF/SpaceNinjaServer#1381
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-03-30 06:20:53 -07:00
fa34b99976 chore: improve login error for unknown email + no auto-create (#1379)
Reviewed-on: OpenWF/SpaceNinjaServer#1379
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-03-30 05:11:17 -07:00
d3819c25c5 feat(webui): gild action for modular equipment (#1375)
Reviewed-on: OpenWF/SpaceNinjaServer#1375
Reviewed-by: Sainan <sainan@calamity.inc>
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-03-30 05:10:32 -07:00
4cb883dabf feat(webui): adding modular K-Drives, Amps and Zaw (#1374)
Reviewed-on: OpenWF/SpaceNinjaServer#1374
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-03-30 05:10:24 -07:00
c376ff25f3 chore: don't emit code when verifying types in CI (#1380)
Reviewed-on: OpenWF/SpaceNinjaServer#1380
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-03-30 05:02:42 -07:00
f7ada5a7e5 chore: delete guild when founding warlord leaves (#1371)
Reviewed-on: OpenWF/SpaceNinjaServer#1371
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-03-30 04:40:00 -07:00
1bdc5126b3 feat: lock worldState time via config (#1361)
Reviewed-on: OpenWF/SpaceNinjaServer#1361
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-03-29 15:42:42 -07:00
a7899d1c18 feat: give kahl standing when completing the new war (#1334)
Reviewed-on: OpenWF/SpaceNinjaServer#1334
2025-03-29 15:35:43 -07:00
895b9381ca chore: update eslint (#1373)
Reviewed-on: OpenWF/SpaceNinjaServer#1373
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-03-29 15:20:54 -07:00
177 changed files with 9135 additions and 3786 deletions

View File

@ -15,17 +15,16 @@
"@typescript-eslint/restrict-template-expressions": "warn", "@typescript-eslint/restrict-template-expressions": "warn",
"@typescript-eslint/restrict-plus-operands": "warn", "@typescript-eslint/restrict-plus-operands": "warn",
"@typescript-eslint/no-unsafe-member-access": "warn", "@typescript-eslint/no-unsafe-member-access": "warn",
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], "@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_", "caughtErrors": "none" }],
"@typescript-eslint/no-misused-promises": "warn",
"@typescript-eslint/no-unsafe-argument": "error", "@typescript-eslint/no-unsafe-argument": "error",
"@typescript-eslint/no-unsafe-call": "warn", "@typescript-eslint/no-unsafe-call": "warn",
"@typescript-eslint/no-unsafe-assignment": "warn", "@typescript-eslint/no-unsafe-assignment": "warn",
"@typescript-eslint/no-explicit-any": "warn", "@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-loss-of-precision": "warn", "no-loss-of-precision": "warn",
"@typescript-eslint/no-unnecessary-condition": "warn", "@typescript-eslint/no-unnecessary-condition": "warn",
"@typescript-eslint/no-base-to-string": "off",
"no-case-declarations": "error", "no-case-declarations": "error",
"prettier/prettier": "error", "prettier/prettier": "error",
"@typescript-eslint/semi": "error",
"no-mixed-spaces-and-tabs": "error", "no-mixed-spaces-and-tabs": "error",
"require-await": "off", "require-await": "off",
"@typescript-eslint/require-await": "error" "@typescript-eslint/require-await": "error"

2
.gitattributes vendored
View File

@ -1,4 +1,4 @@
# Auto detect text files and perform LF normalization # Auto detect text files and perform LF normalization
* text=auto * text=auto eol=lf
static/webui/libs/ linguist-vendored static/webui/libs/ linguist-vendored

View File

@ -5,17 +5,22 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
version: [18, 20, 22]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4.1.2 uses: actions/checkout@v4.1.2
- name: Setup Node.js environment - name: Setup Node.js environment
uses: actions/setup-node@v4.0.2 uses: actions/setup-node@v4.0.2
with:
node-version: ${{ matrix.version }}
- run: npm ci - run: npm ci
- run: cp config.json.example config.json - run: cp config.json.example config.json
- run: npm run build - run: npm run verify
- run: npm run lint - run: npm run lint:ci
- run: npm run prettier
- run: npm run update-translations
- name: Fail if there are uncommitted changes
run: |
if [[ -n "$(git status --porcelain)" ]]; then
echo "Uncommitted changes detected:"
git status
git --no-pager diff
exit 1
fi

View File

@ -1,3 +1,4 @@
src/routes/api.ts
static/webui/libs/ static/webui/libs/
*.html *.html
*.md *.md

3
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["dbaeumer.vscode-eslint"]
}

View File

@ -10,8 +10,6 @@ ENV APP_SKIP_TUTORIAL=true
ENV APP_SKIP_ALL_DIALOGUE=true ENV APP_SKIP_ALL_DIALOGUE=true
ENV APP_UNLOCK_ALL_SCANS=true ENV APP_UNLOCK_ALL_SCANS=true
ENV APP_UNLOCK_ALL_MISSIONS=true ENV APP_UNLOCK_ALL_MISSIONS=true
ENV APP_UNLOCK_ALL_QUESTS=true
ENV APP_COMPLETE_ALL_QUESTS=true
ENV APP_INFINITE_RESOURCES=true ENV APP_INFINITE_RESOURCES=true
ENV APP_UNLOCK_ALL_SHIP_FEATURES=true ENV APP_UNLOCK_ALL_SHIP_FEATURES=true
ENV APP_UNLOCK_ALL_SHIP_DECORATIONS=true ENV APP_UNLOCK_ALL_SHIP_DECORATIONS=true

View File

@ -10,5 +10,8 @@ To get an idea of what functionality you can expect to be missing [have a look t
## config.json ## 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.
- `logger.level` can be `fatal`, `error`, `warn`, `info`, `http`, `debug`, or `trace`. - `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 ]`. - `myIrcAddresses` can be used to point to an IRC server. If not provided, defaults to `[ myAddress ]`.
- `worldState.lockTime` will lock the time provided in worldState if nonzero, e.g. `1743202800` for night in POE.

View File

@ -29,19 +29,26 @@
"unlockExilusEverywhere": false, "unlockExilusEverywhere": false,
"unlockArcanesEverywhere": false, "unlockArcanesEverywhere": false,
"noDailyStandingLimits": false, "noDailyStandingLimits": false,
"noDailyFocusLimit": false,
"noArgonCrystalDecay": false, "noArgonCrystalDecay": false,
"noMasteryRankUpCooldown": false,
"noVendorPurchaseLimits": true, "noVendorPurchaseLimits": true,
"noDeathMarks": false,
"noKimCooldowns": false,
"instantResourceExtractorDrones": false, "instantResourceExtractorDrones": false,
"noResourceExtractorDronesDamage": false,
"noDojoRoomBuildStage": false, "noDojoRoomBuildStage": false,
"noDecoBuildStage": false,
"fastDojoRoomDestruction": false, "fastDojoRoomDestruction": false,
"noDojoResearchCosts": false, "noDojoResearchCosts": false,
"noDojoResearchTime": false, "noDojoResearchTime": false,
"fastClanAscension": false, "fastClanAscension": false,
"spoofMasteryRank": -1, "spoofMasteryRank": -1,
"events": { "worldState": {
"creditBoost": false, "creditBoost": false,
"affinityBoost": false, "affinityBoost": false,
"resourceBoost": false, "resourceBoost": false,
"starDays": true "starDays": true,
"lockTime": 0
} }
} }

638
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,10 +4,12 @@
"description": "WF Emulator", "description": "WF Emulator",
"main": "index.ts", "main": "index.ts",
"scripts": { "scripts": {
"start": "node --import ./build/src/pathman.js build/src/index.js", "start": "node --enable-source-maps --import ./build/src/pathman.js build/src/index.js",
"dev": "ts-node-dev --openssl-legacy-provider -r tsconfig-paths/register src/index.ts ", "dev": "ts-node-dev --openssl-legacy-provider -r tsconfig-paths/register src/index.ts ",
"build": "tsc && copyfiles static/webui/** build", "build": "tsc --incremental --sourceMap && ncp static/webui build/static/webui",
"verify": "tsgo --noEmit",
"lint": "eslint --ext .ts .", "lint": "eslint --ext .ts .",
"lint:ci": "eslint --ext .ts --rule \"prettier/prettier: off\" .",
"lint:fix": "eslint --fix --ext .ts .", "lint:fix": "eslint --fix --ext .ts .",
"prettier": "prettier --write .", "prettier": "prettier --write .",
"update-translations": "cd scripts && node update-translations.js" "update-translations": "cd scripts && node update-translations.js"
@ -16,24 +18,25 @@
"dependencies": { "dependencies": {
"@types/express": "^5", "@types/express": "^5",
"@types/morgan": "^1.9.9", "@types/morgan": "^1.9.9",
"copyfiles": "^2.4.1",
"crc-32": "^1.2.2", "crc-32": "^1.2.2",
"express": "^5", "express": "^5",
"json-with-bigint": "^3.2.2", "json-with-bigint": "^3.2.2",
"mongoose": "^8.11.0", "mongoose": "^8.11.0",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"typescript": ">=5.5 <5.6.0", "ncp": "^2.0.0",
"warframe-public-export-plus": "^0.5.48", "typescript": "^5.5",
"warframe-public-export-plus": "^0.5.56",
"warframe-riven-info": "^0.1.2", "warframe-riven-info": "^0.1.2",
"winston": "^3.17.0", "winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0" "winston-daily-rotate-file": "^5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^7.18", "@rxliuli/tsgo": "^2025.3.31",
"@typescript-eslint/parser": "^7.18", "@typescript-eslint/eslint-plugin": "^8.28.0",
"eslint": "^8.56.0", "@typescript-eslint/parser": "^8.28.0",
"eslint-plugin-prettier": "^5.2.3", "eslint": "^8",
"prettier": "^3.4.2", "eslint-plugin-prettier": "^5.2.5",
"prettier": "^3.5.3",
"ts-node-dev": "^2.0.0", "ts-node-dev": "^2.0.0",
"tsconfig-paths": "^4.2.0" "tsconfig-paths": "^4.2.0"
}, },

View File

@ -4,7 +4,7 @@
const fs = require("fs"); const fs = require("fs");
function extractStrings(content) { function extractStrings(content) {
const regex = /([a-zA-Z_]+): `([^`]*)`,/g; const regex = /([a-zA-Z0-9_]+): `([^`]*)`,/g;
let matches; let matches;
const strings = {}; const strings = {};
while ((matches = regex.exec(content)) !== null) { while ((matches = regex.exec(content)) !== null) {
@ -15,7 +15,7 @@ function extractStrings(content) {
const source = fs.readFileSync("../static/webui/translations/en.js", "utf8"); const source = fs.readFileSync("../static/webui/translations/en.js", "utf8");
const sourceStrings = extractStrings(source); const sourceStrings = extractStrings(source);
const sourceLines = source.split("\n"); const sourceLines = source.substring(0, source.length - 1).split("\n");
fs.readdirSync("../static/webui/translations").forEach(file => { fs.readdirSync("../static/webui/translations").forEach(file => {
if (fs.lstatSync(`../static/webui/translations/${file}`).isFile() && file !== "en.js") { if (fs.lstatSync(`../static/webui/translations/${file}`).isFile() && file !== "en.js") {
@ -36,7 +36,7 @@ fs.readdirSync("../static/webui/translations").forEach(file => {
fs.writeSync(fileHandle, ` ${key}: \`[UNTRANSLATED] ${value}\`,\n`); fs.writeSync(fileHandle, ` ${key}: \`[UNTRANSLATED] ${value}\`,\n`);
} }
}); });
} else if (line.length) { } else {
fs.writeSync(fileHandle, line + "\n"); fs.writeSync(fileHandle, line + "\n");
} }
}); });

View File

@ -16,9 +16,9 @@ import { webuiRouter } from "@/src/routes/webui";
const app = express(); const app = express();
app.use((req, _res, next) => { app.use((req, _res, next) => {
// 38.5.0 introduced "ezip" for encrypted body blobs. // 38.5.0 introduced "ezip" for encrypted body blobs and "e" for request verification only (encrypted body blobs with no application data).
// The bootstrapper decrypts it for us but having an unsupported Content-Encoding here would still be an issue for Express, so removing it. // The bootstrapper decrypts it for us but having an unsupported Content-Encoding here would still be an issue for Express, so removing it.
if (req.headers["content-encoding"] == "ezip") { if (req.headers["content-encoding"] == "ezip" || req.headers["content-encoding"] == "e") {
req.headers["content-encoding"] = undefined; req.headers["content-encoding"] = undefined;
} }
next(); next();

View File

@ -32,7 +32,7 @@ export const abortDojoComponentController: RequestHandler = async (req, res) =>
if (request.DecoId) { if (request.DecoId) {
removeDojoDeco(guild, request.ComponentId, request.DecoId); removeDojoDeco(guild, request.ComponentId, request.DecoId);
} else { } else {
removeDojoRoom(guild, request.ComponentId); await removeDojoRoom(guild, request.ComponentId);
} }
await guild.save(); await guild.save();

View File

@ -0,0 +1,30 @@
import { toOid } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Account, Ignore } from "@/src/models/loginModel";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IFriendInfo } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
export const addIgnoredUserController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const data = getJSONfromString<IAddIgnoredUserRequest>(String(req.body));
const ignoreeAccount = await Account.findOne(
{ DisplayName: data.playerName.substring(0, data.playerName.length - 1) },
"_id"
);
if (ignoreeAccount) {
await Ignore.create({ ignorer: accountId, ignoree: ignoreeAccount._id });
res.json({
Ignored: {
_id: toOid(ignoreeAccount._id),
DisplayName: data.playerName
} satisfies IFriendInfo
});
} else {
res.status(400).end();
}
};
interface IAddIgnoredUserRequest {
playerName: string;
}

View File

@ -0,0 +1,117 @@
import { getJSONfromString, regexEscape } from "@/src/helpers/stringHelpers";
import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel";
import { createMessage } from "@/src/services/inboxService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express";
import { ExportFlavour } from "warframe-public-export-plus";
export const addToAllianceController: RequestHandler = async (req, res) => {
// Check requester is a warlord in their guild
const account = await getAccountForRequest(req);
const guildMember = (await GuildMember.findOne({ accountId: account._id, status: 0 }))!;
if (guildMember.rank > 1) {
res.status(400).json({ Error: 104 });
return;
}
// Check guild has invite permissions in the alliance
const allianceMember = (await AllianceMember.findOne({
allianceId: req.query.allianceId,
guildId: guildMember.guildId
}))!;
if (!(allianceMember.Permissions & GuildPermission.Recruiter)) {
res.status(400).json({ Error: 104 });
return;
}
// Find clan to invite
const payload = getJSONfromString<IAddToAllianceRequest>(String(req.body));
const guilds = await Guild.find(
{
Name:
payload.clanName.indexOf("#") == -1
? new RegExp("^" + regexEscape(payload.clanName) + "#...$")
: payload.clanName
},
"Name"
);
if (guilds.length == 0) {
res.status(400).json({ Error: 101 });
return;
}
if (guilds.length > 1) {
const choices: IGuildChoice[] = [];
for (const guild of guilds) {
choices.push({
OriginalPlatform: 0,
Name: guild.Name
});
}
res.json(choices);
return;
}
// Add clan as a pending alliance member
try {
await AllianceMember.insertOne({
allianceId: req.query.allianceId,
guildId: guilds[0]._id,
Pending: true,
Permissions: 0
});
} catch (e) {
logger.debug(`alliance invite failed due to ${String(e)}`);
res.status(400).json({ Error: 102 });
return;
}
// Send inbox message to founding warlord
// TOVERIFY: Should other warlords get this as well?
// TOVERIFY: Who/what should the sender be?
// TOVERIFY: Should this message be highPriority?
const invitedClanOwnerMember = (await GuildMember.findOne({ guildId: guilds[0]._id, rank: 0 }))!;
const senderInventory = await getInventory(account._id.toString(), "ActiveAvatarImageType");
const senderGuild = (await Guild.findById(allianceMember.guildId, "Name"))!;
const alliance = (await Alliance.findById(req.query.allianceId, "Name"))!;
await createMessage(invitedClanOwnerMember.accountId, [
{
sndr: getSuffixedName(account),
msg: "/Lotus/Language/Menu/Mailbox_AllianceInvite_Body",
arg: [
{
Key: "THEIR_CLAN",
Tag: senderGuild.Name
},
{
Key: "CLAN",
Tag: guilds[0].Name
},
{
Key: "ALLIANCE",
Tag: alliance.Name
}
],
sub: "/Lotus/Language/Menu/Mailbox_AllianceInvite_Title",
icon: ExportFlavour[senderInventory.ActiveAvatarImageType].icon,
contextInfo: alliance._id.toString(),
highPriority: true,
acceptAction: "ALLIANCE_INVITE",
declineAction: "ALLIANCE_INVITE",
hasAccountAction: true
}
]);
res.end();
};
interface IAddToAllianceRequest {
clanName: string;
}
interface IGuildChoice {
OriginalPlatform: number;
Name: string;
}

View File

@ -3,15 +3,19 @@ import { Account } from "@/src/models/loginModel";
import { fillInInventoryDataForGuildMember, hasGuildPermission } from "@/src/services/guildService"; import { fillInInventoryDataForGuildMember, hasGuildPermission } from "@/src/services/guildService";
import { createMessage } from "@/src/services/inboxService"; import { createMessage } from "@/src/services/inboxService";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; import { getAccountForRequest, getAccountIdForRequest, getSuffixedName } from "@/src/services/loginService";
import { IOid } from "@/src/types/commonTypes"; import { IOid } from "@/src/types/commonTypes";
import { GuildPermission, IGuildMemberClient } from "@/src/types/guildTypes"; import { GuildPermission, IGuildMemberClient } from "@/src/types/guildTypes";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { ExportFlavour } from "warframe-public-export-plus"; import { ExportFlavour } from "warframe-public-export-plus";
export const addToGuildController: RequestHandler = async (req, res) => { export const addToGuildController: RequestHandler = async (req, res) => {
const payload = JSON.parse(String(req.body)) as IAddToGuildRequest; const payload = JSON.parse(String(req.body)) as IAddToGuildRequest;
if ("UserName" in payload) {
// Clan recruiter sending an invite
const account = await Account.findOne({ DisplayName: payload.UserName }); const account = await Account.findOne({ DisplayName: payload.UserName });
if (!account) { if (!account) {
res.status(400).json("Username does not exist"); res.status(400).json("Username does not exist");
@ -31,24 +35,20 @@ export const addToGuildController: RequestHandler = async (req, res) => {
res.status(400).json("Invalid permission"); res.status(400).json("Invalid permission");
} }
if ( try {
await GuildMember.exists({
accountId: account._id,
guildId: payload.GuildId.$oid
})
) {
res.status(400).json("User already invited to clan");
return;
}
await GuildMember.insertOne({ await GuildMember.insertOne({
accountId: account._id, accountId: account._id,
guildId: payload.GuildId.$oid, guildId: payload.GuildId.$oid,
status: 2 // outgoing invite status: 2 // outgoing invite
}); });
} catch (e) {
logger.debug(`guild invite failed due to ${String(e)}`);
res.status(400).json("User already invited to clan");
return;
}
const senderInventory = await getInventory(senderAccount._id.toString(), "ActiveAvatarImageType"); const senderInventory = await getInventory(senderAccount._id.toString(), "ActiveAvatarImageType");
await createMessage(account._id.toString(), [ await createMessage(account._id, [
{ {
sndr: getSuffixedName(senderAccount), sndr: getSuffixedName(senderAccount),
msg: "/Lotus/Language/Menu/Mailbox_ClanInvite_Body", msg: "/Lotus/Language/Menu/Mailbox_ClanInvite_Body",
@ -76,9 +76,30 @@ export const addToGuildController: RequestHandler = async (req, res) => {
}; };
await fillInInventoryDataForGuildMember(member); await fillInInventoryDataForGuildMember(member);
res.json({ NewMember: member }); res.json({ NewMember: member });
} else if ("RequestMsg" in payload) {
// Player applying to join a clan
const accountId = await getAccountIdForRequest(req);
try {
await GuildMember.insertOne({
accountId,
guildId: payload.GuildId.$oid,
status: 1, // incoming invite
RequestMsg: payload.RequestMsg,
RequestExpiry: new Date(Date.now() + 14 * 86400 * 1000) // TOVERIFY: I can't find any good information about this with regards to live, but 2 weeks seem reasonable.
});
} catch (e) {
logger.debug(`guild invite failed due to ${String(e)}`);
res.status(400).send("Already requested");
}
res.end();
} else {
logger.error(`data provided to ${req.path}: ${String(req.body)}`);
res.status(400).end();
}
}; };
interface IAddToGuildRequest { interface IAddToGuildRequest {
UserName: string; UserName?: string;
GuildId: IOid; GuildId: IOid;
RequestMsg?: string;
} }

View File

@ -57,12 +57,16 @@ export const artifactTransmutationController: RequestHandler = async (req, res)
payload.Consumed.forEach(upgrade => { payload.Consumed.forEach(upgrade => {
const meta = ExportUpgrades[upgrade.ItemType]; const meta = ExportUpgrades[upgrade.ItemType];
counts[meta.rarity] += upgrade.ItemCount; counts[meta.rarity] += upgrade.ItemCount;
if (upgrade.ItemId.$oid != "000000000000000000000000") {
inventory.Upgrades.pull({ _id: upgrade.ItemId.$oid });
} else {
addMods(inventory, [ addMods(inventory, [
{ {
ItemType: upgrade.ItemType, ItemType: upgrade.ItemType,
ItemCount: upgrade.ItemCount * -1 ItemCount: upgrade.ItemCount * -1
} }
]); ]);
}
if (upgrade.ItemType == "/Lotus/Upgrades/Mods/TransmuteCores/AttackTransmuteCore") { if (upgrade.ItemType == "/Lotus/Upgrades/Mods/TransmuteCores/AttackTransmuteCore") {
forcedPolarity = "AP_ATTACK"; forcedPolarity = "AP_ATTACK";
} else if (upgrade.ItemType == "/Lotus/Upgrades/Mods/TransmuteCores/DefenseTransmuteCore") { } else if (upgrade.ItemType == "/Lotus/Upgrades/Mods/TransmuteCores/DefenseTransmuteCore") {
@ -72,6 +76,15 @@ export const artifactTransmutationController: RequestHandler = async (req, res)
} }
}); });
let newModType: string | undefined;
for (const specialModSet of specialModSets) {
if (specialModSet.indexOf(payload.Consumed[0].ItemType) != -1) {
newModType = getRandomElement(specialModSet);
break;
}
}
if (!newModType) {
// Based on the table on https://wiki.warframe.com/w/Transmutation // Based on the table on https://wiki.warframe.com/w/Transmutation
const weights: Record<TRarity, number> = { const weights: Record<TRarity, number> = {
COMMON: counts.COMMON * 95 + counts.UNCOMMON * 15 + counts.RARE * 4, COMMON: counts.COMMON * 95 + counts.UNCOMMON * 15 + counts.RARE * 4,
@ -87,7 +100,9 @@ export const artifactTransmutationController: RequestHandler = async (req, res)
} }
}); });
const newModType = getRandomWeightedReward(options, weights)!.uniqueName; newModType = getRandomWeightedReward(options, weights)!.uniqueName;
}
addMods(inventory, [ addMods(inventory, [
{ {
ItemType: newModType, ItemType: newModType,
@ -130,3 +145,34 @@ interface IAgnosticUpgradeClient {
ItemCount: number; ItemCount: number;
LastAdded: IOid; LastAdded: IOid;
} }
const specialModSets: string[][] = [
[
"/Lotus/Upgrades/Mods/Immortal/ImmortalOneMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalTwoMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalThreeMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalFourMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalFiveMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalSixMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalSevenMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalEightMod",
"/Lotus/Upgrades/Mods/Immortal/ImmortalWildcardMod"
],
[
"/Lotus/Upgrades/Mods/Immortal/AntivirusOneMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusTwoMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusThreeMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusFourMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusFiveMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusSixMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusSevenMod",
"/Lotus/Upgrades/Mods/Immortal/AntivirusEightMod"
],
[
"/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndSpeedOnUseMod",
"/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndWeaponDamageOnUseMod",
"/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusLargeOnSingleUseMod",
"/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusOnUseMod",
"/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod"
]
];

View File

@ -0,0 +1,20 @@
import { GuildAd } from "@/src/models/guildModel";
import { getGuildForRequestEx, hasGuildPermission } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
export const cancelGuildAdvertisementController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId");
const guild = await getGuildForRequestEx(req, inventory);
if (!(await hasGuildPermission(guild, accountId, GuildPermission.Advertiser))) {
res.status(400).end();
return;
}
await GuildAd.deleteOne({ GuildId: guild._id });
res.end();
};

View File

@ -15,6 +15,12 @@ export const changeDojoRootController: RequestHandler = async (req, res) => {
return; return;
} }
// Example POST body: {"pivot":[0, 0, -64],"components":"{\"670429301ca0a63848ccc467\":{\"R\":[0,0,0],\"P\":[0,3,32]},\"6704254a1ca0a63848ccb33c\":{\"R\":[0,0,0],\"P\":[0,9.25,-32]},\"670429461ca0a63848ccc731\":{\"R\":[-90,0,0],\"P\":[-47.999992370605,3,16]}}"}
if (req.body) {
logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
throw new Error("dojo reparent operation should not need deco repositioning"); // because we always provide SortId
}
const idToNode: Record<string, INode> = {}; const idToNode: Record<string, INode> = {};
guild.DojoComponents.forEach(x => { guild.DojoComponents.forEach(x => {
idToNode[x._id.toString()] = { idToNode[x._id.toString()] = {
@ -43,23 +49,13 @@ export const changeDojoRootController: RequestHandler = async (req, res) => {
newRoot.component.pp = undefined; newRoot.component.pp = undefined;
newRoot.parent = undefined; newRoot.parent = undefined;
// Don't even ask me why this is needed because I don't know either // Set/update SortId in top-to-bottom order
const stack: INode[] = [newRoot]; const stack: INode[] = [newRoot];
let i = 0;
const idMap: Record<string, Types.ObjectId> = {};
while (stack.length != 0) { while (stack.length != 0) {
const top = stack.shift()!; const top = stack.shift()!;
idMap[top.component._id.toString()] = new Types.ObjectId( top.component.SortId = new Types.ObjectId();
(++i).toString(16).padStart(8, "0") + top.component._id.toString().substr(8)
);
top.children.forEach(x => stack.push(x)); top.children.forEach(x => stack.push(x));
} }
guild.DojoComponents.forEach(x => {
x._id = idMap[x._id.toString()];
if (x.pi) {
x.pi = idMap[x.pi.toString()];
}
});
logger.debug("New tree:\n" + treeToString(newRoot)); logger.debug("New tree:\n" + treeToString(newRoot));

View File

@ -18,8 +18,9 @@ import {
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { toOid } from "@/src/helpers/inventoryHelpers";
export interface IClaimCompletedRecipeRequest { interface IClaimCompletedRecipeRequest {
RecipeIds: IOid[]; RecipeIds: IOid[];
} }
@ -80,6 +81,7 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
} else { } else {
logger.debug("Claiming Recipe", { recipe, pendingRecipe }); logger.debug("Claiming Recipe", { recipe, pendingRecipe });
let BrandedSuits: undefined | IOid[];
if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") { if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
inventory.PendingSpectreLoadouts ??= []; inventory.PendingSpectreLoadouts ??= [];
inventory.SpectreLoadouts ??= []; inventory.SpectreLoadouts ??= [];
@ -99,9 +101,15 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
inventory.SpectreLoadouts.push(inventory.PendingSpectreLoadouts[pendingLoadoutIndex]); inventory.SpectreLoadouts.push(inventory.PendingSpectreLoadouts[pendingLoadoutIndex]);
inventory.PendingSpectreLoadouts.splice(pendingLoadoutIndex, 1); inventory.PendingSpectreLoadouts.splice(pendingLoadoutIndex, 1);
} }
} else if (recipe.secretIngredientAction == "SIA_UNBRAND") {
inventory.BrandedSuits!.splice(
inventory.BrandedSuits!.findIndex(x => x.equals(pendingRecipe.SuitToUnbrand)),
1
);
BrandedSuits = [toOid(pendingRecipe.SuitToUnbrand!)];
} }
let InventoryChanges = {}; let InventoryChanges: IInventoryChanges = {};
if (recipe.consumeOnUse) { if (recipe.consumeOnUse) {
addRecipes(inventory, [ addRecipes(inventory, [
{ {
@ -111,16 +119,24 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
]); ]);
} }
if (req.query.rush) { if (req.query.rush) {
const end = Math.trunc(pendingRecipe.CompletionDate.getTime() / 1000);
const start = end - recipe.buildTime;
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)));
InventoryChanges = { InventoryChanges = {
...InventoryChanges, ...InventoryChanges,
...updateCurrency(inventory, recipe.skipBuildTimePrice, true) ...updateCurrency(inventory, cost, true)
}; };
} }
if (recipe.secretIngredientAction != "SIA_UNBRAND") {
InventoryChanges = { InventoryChanges = {
...InventoryChanges, ...InventoryChanges,
...(await addItem(inventory, recipe.resultType, recipe.num, false)) ...(await addItem(inventory, recipe.resultType, recipe.num, false))
}; };
}
await inventory.save(); await inventory.save();
res.json({ InventoryChanges }); res.json({ InventoryChanges, BrandedSuits });
} }
}; };

View File

@ -7,6 +7,8 @@ export const clearDialogueHistoryController: RequestHandler = async (req, res) =
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const request = JSON.parse(String(req.body)) as IClearDialogueRequest; const request = JSON.parse(String(req.body)) as IClearDialogueRequest;
if (inventory.DialogueHistory && inventory.DialogueHistory.Dialogues) { if (inventory.DialogueHistory && inventory.DialogueHistory.Dialogues) {
inventory.DialogueHistory.Resets ??= 0;
inventory.DialogueHistory.Resets += 1;
for (const dialogueName of request.Dialogues) { for (const dialogueName of request.Dialogues) {
const index = inventory.DialogueHistory.Dialogues.findIndex(x => x.DialogueName == dialogueName); const index = inventory.DialogueHistory.Dialogues.findIndex(x => x.DialogueName == dialogueName);
if (index != -1) { if (index != -1) {

View File

@ -0,0 +1,37 @@
import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel";
import { getAllianceClient } from "@/src/services/guildService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const confirmAllianceInvitationController: RequestHandler = async (req, res) => {
// Check requester is a warlord in their guild
const accountId = await getAccountIdForRequest(req);
const guildMember = (await GuildMember.findOne({ accountId, status: 0 }))!;
if (guildMember.rank > 1) {
res.status(400).json({ Error: 104 });
return;
}
const allianceMember = await AllianceMember.findOne({
allianceId: req.query.allianceId,
guildId: guildMember.guildId
});
if (!allianceMember || !allianceMember.Pending) {
res.status(400);
return;
}
allianceMember.Pending = false;
const guild = (await Guild.findById(guildMember.guildId))!;
guild.AllianceId = allianceMember.allianceId;
await Promise.all([allianceMember.save(), guild.save()]);
// Give client the new alliance data which uses "AllianceId" instead of "_id" in this response
const alliance = (await Alliance.findById(allianceMember.allianceId))!;
const { _id, ...rest } = await getAllianceClient(alliance, guild);
res.json({
AllianceId: _id,
...rest
});
};

View File

@ -1,26 +1,59 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Guild, GuildMember } from "@/src/models/guildModel"; import { Guild, GuildMember } from "@/src/models/guildModel";
import { getGuildClient, updateInventoryForConfirmedGuildJoin } from "@/src/services/guildService"; import { Account } from "@/src/models/loginModel";
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; import { deleteGuild, getGuildClient, hasGuildPermission, removeDojoKeyItems } from "@/src/services/guildService";
import { addRecipes, combineInventoryChanges, getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest, getAccountIdForRequest, getSuffixedName } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
export const confirmGuildInvitationController: RequestHandler = async (req, res) => { // GET request: A player accepting an invite they got in their inbox.
export const confirmGuildInvitationGetController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req); const account = await getAccountForRequest(req);
const guildMember = await GuildMember.findOne({ const invitedGuildMember = await GuildMember.findOne({
accountId: account._id, accountId: account._id,
guildId: req.query.clanId as string guildId: req.query.clanId as string
}); });
if (guildMember) { if (invitedGuildMember && invitedGuildMember.status == 2) {
guildMember.status = 0; let inventoryChanges: IInventoryChanges = {};
await guildMember.save();
await updateInventoryForConfirmedGuildJoin( // If this account is already in a guild, we need to do cleanup first.
account._id.toString(), const guildMember = await GuildMember.findOneAndDelete({ accountId: account._id, status: 0 });
new Types.ObjectId(req.query.clanId as string) if (guildMember) {
); const inventory = await getInventory(account._id.toString(), "LevelKeys Recipes");
inventoryChanges = removeDojoKeyItems(inventory);
await inventory.save();
if (guildMember.rank == 0) {
await deleteGuild(guildMember.guildId);
}
}
// Now that we're sure this account is not in a guild right now, we can just proceed with the normal updates.
invitedGuildMember.status = 0;
await invitedGuildMember.save();
// Remove pending applications for this account
await GuildMember.deleteMany({ accountId: account._id, status: 1 });
// Update inventory of new member
const inventory = await getInventory(account._id.toString(), "GuildId LevelKeys Recipes");
inventory.GuildId = new Types.ObjectId(req.query.clanId as string);
const recipeChanges = [
{
ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint",
ItemCount: 1
}
];
addRecipes(inventory, recipeChanges);
combineInventoryChanges(inventoryChanges, { Recipes: recipeChanges });
await inventory.save();
const guild = (await Guild.findById(req.query.clanId as string))!; const guild = (await Guild.findById(req.query.clanId as string))!;
// Add join to clan log
guild.RosterActivity ??= []; guild.RosterActivity ??= [];
guild.RosterActivity.push({ guild.RosterActivity.push({
dateTime: new Date(), dateTime: new Date(),
@ -31,16 +64,61 @@ export const confirmGuildInvitationController: RequestHandler = async (req, res)
res.json({ res.json({
...(await getGuildClient(guild, account._id.toString())), ...(await getGuildClient(guild, account._id.toString())),
InventoryChanges: { InventoryChanges: inventoryChanges
Recipes: [
{
ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint",
ItemCount: 1
}
]
}
}); });
} else { } else {
res.end(); res.end();
} }
}; };
// POST request: Clan representative accepting invite(s).
export const confirmGuildInvitationPostController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const guild = (await Guild.findById(req.query.clanId as string, "Ranks RosterActivity"))!;
if (!(await hasGuildPermission(guild, accountId, GuildPermission.Recruiter))) {
res.status(400).json("Invalid permission");
return;
}
const payload = getJSONfromString<{ userId: string }>(String(req.body));
const filter: { accountId?: string; status: number } = { status: 1 };
if (payload.userId != "all") {
filter.accountId = payload.userId;
}
const guildMembers = await GuildMember.find(filter);
const newMembers: string[] = [];
for (const guildMember of guildMembers) {
guildMember.status = 0;
guildMember.RequestMsg = undefined;
guildMember.RequestExpiry = undefined;
await guildMember.save();
// Remove other pending applications for this account
await GuildMember.deleteMany({ accountId: guildMember.accountId, status: 1 });
// Update inventory of new member
const inventory = await getInventory(guildMember.accountId.toString(), "GuildId Recipes");
inventory.GuildId = new Types.ObjectId(req.query.clanId as string);
addRecipes(inventory, [
{
ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint",
ItemCount: 1
}
]);
await inventory.save();
// Add join to clan log
const account = (await Account.findOne({ _id: guildMember.accountId }))!;
guild.RosterActivity ??= [];
guild.RosterActivity.push({
dateTime: new Date(),
entryType: 6,
details: getSuffixedName(account)
});
newMembers.push(account._id.toString());
}
await guild.save();
res.json({
NewMembers: newMembers
});
};

View File

@ -1,8 +1,7 @@
import { toMongoDate } from "@/src/helpers/inventoryHelpers"; import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Guild, GuildMember } from "@/src/models/guildModel"; import { Guild } from "@/src/models/guildModel";
import { config } from "@/src/services/configService"; import { checkClanAscensionHasRequiredContributors } from "@/src/services/guildService";
import { createMessage } from "@/src/services/inboxService";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
@ -31,43 +30,7 @@ export const contributeGuildClassController: RequestHandler = async (req, res) =
guild.CeremonyContributors.push(new Types.ObjectId(accountId)); guild.CeremonyContributors.push(new Types.ObjectId(accountId));
// Once required contributor count is hit, the class is committed and there's 72 hours to claim endo. await checkClanAscensionHasRequiredContributors(guild);
if (guild.CeremonyContributors.length == payload.RequiredContributors) {
guild.Class = guild.CeremonyClass!;
guild.CeremonyClass = undefined;
guild.CeremonyResetDate = new Date(Date.now() + (config.fastClanAscension ? 5_000 : 72 * 3600_000));
if (!config.fastClanAscension) {
// Send message to all active guild members
const members = await GuildMember.find({ guildId: payload.GuildId, status: 0 }, "accountId");
for (const member of members) {
// somewhat unfaithful as on live the "msg" is not a loctag, but since we don't have the string, we'll let the client fill it in with "arg".
await createMessage(member.accountId.toString(), [
{
sndr: guild.Name,
msg: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgressDetails",
arg: [
{
Key: "RESETDATE",
Tag:
guild.CeremonyResetDate.getUTCMonth() +
"/" +
guild.CeremonyResetDate.getUTCDate() +
"/" +
(guild.CeremonyResetDate.getUTCFullYear() % 100) +
" " +
guild.CeremonyResetDate.getUTCHours().toString().padStart(2, "0") +
":" +
guild.CeremonyResetDate.getUTCMinutes().toString().padStart(2, "0")
}
],
sub: "/Lotus/Language/Clan/Clan_AscensionCeremonyInProgress",
icon: "/Lotus/Interface/Graphics/ClanTileImages/ClanEnterDojo.png",
highPriority: true
}
]);
}
}
}
await guild.save(); await guild.save();

View File

@ -1,6 +1,7 @@
import { GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel"; import { GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { import {
addGuildMemberMiscItemContribution,
getDojoClient, getDojoClient,
getGuildForRequestEx, getGuildForRequestEx,
hasAccessToDojo, hasAccessToDojo,
@ -63,9 +64,7 @@ export const contributeToDojoComponentController: RequestHandler = async (req, r
} }
} }
await guild.save(); await Promise.all([guild.save(), inventory.save(), guildMember.save()]);
await inventory.save();
await guildMember.save();
res.json({ res.json({
...(await getDojoClient(guild, 0, component._id)), ...(await getDojoClient(guild, 0, component._id)),
InventoryChanges: inventoryChanges InventoryChanges: inventoryChanges
@ -94,10 +93,10 @@ const processContribution = (
component.RegularCredits += request.VaultCredits; component.RegularCredits += request.VaultCredits;
guild.VaultRegularCredits! -= request.VaultCredits; guild.VaultRegularCredits! -= request.VaultCredits;
} }
if (component.RegularCredits > scaleRequiredCount(meta.price)) { if (component.RegularCredits > scaleRequiredCount(guild.Tier, meta.price)) {
guild.VaultRegularCredits ??= 0; guild.VaultRegularCredits ??= 0;
guild.VaultRegularCredits += component.RegularCredits - scaleRequiredCount(meta.price); guild.VaultRegularCredits += component.RegularCredits - scaleRequiredCount(guild.Tier, meta.price);
component.RegularCredits = scaleRequiredCount(meta.price); component.RegularCredits = scaleRequiredCount(guild.Tier, meta.price);
} }
component.MiscItems ??= []; component.MiscItems ??= [];
@ -108,10 +107,10 @@ const processContribution = (
const ingredientMeta = meta.ingredients.find(x => x.ItemType == ingredientContribution.ItemType)!; const ingredientMeta = meta.ingredients.find(x => x.ItemType == ingredientContribution.ItemType)!;
if ( if (
componentMiscItem.ItemCount + ingredientContribution.ItemCount > componentMiscItem.ItemCount + ingredientContribution.ItemCount >
scaleRequiredCount(ingredientMeta.ItemCount) scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount)
) { ) {
ingredientContribution.ItemCount = ingredientContribution.ItemCount =
scaleRequiredCount(ingredientMeta.ItemCount) - componentMiscItem.ItemCount; scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount) - componentMiscItem.ItemCount;
} }
componentMiscItem.ItemCount += ingredientContribution.ItemCount; componentMiscItem.ItemCount += ingredientContribution.ItemCount;
} else { } else {
@ -129,10 +128,10 @@ const processContribution = (
const ingredientMeta = meta.ingredients.find(x => x.ItemType == ingredientContribution.ItemType)!; const ingredientMeta = meta.ingredients.find(x => x.ItemType == ingredientContribution.ItemType)!;
if ( if (
componentMiscItem.ItemCount + ingredientContribution.ItemCount > componentMiscItem.ItemCount + ingredientContribution.ItemCount >
scaleRequiredCount(ingredientMeta.ItemCount) scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount)
) { ) {
ingredientContribution.ItemCount = ingredientContribution.ItemCount =
scaleRequiredCount(ingredientMeta.ItemCount) - componentMiscItem.ItemCount; scaleRequiredCount(guild.Tier, ingredientMeta.ItemCount) - componentMiscItem.ItemCount;
} }
componentMiscItem.ItemCount += ingredientContribution.ItemCount; componentMiscItem.ItemCount += ingredientContribution.ItemCount;
} else { } else {
@ -143,18 +142,20 @@ const processContribution = (
ItemCount: ingredientContribution.ItemCount * -1 ItemCount: ingredientContribution.ItemCount * -1
}); });
guildMember.MiscItemsContributed ??= []; addGuildMemberMiscItemContribution(guildMember, ingredientContribution);
guildMember.MiscItemsContributed.push(ingredientContribution);
} }
addMiscItems(inventory, miscItemChanges); addMiscItems(inventory, miscItemChanges);
inventoryChanges.MiscItems = miscItemChanges; inventoryChanges.MiscItems = miscItemChanges;
} }
if (component.RegularCredits >= scaleRequiredCount(meta.price)) { if (component.RegularCredits >= scaleRequiredCount(guild.Tier, meta.price)) {
let fullyFunded = true; let fullyFunded = true;
for (const ingredient of meta.ingredients) { for (const ingredient of meta.ingredients) {
const componentMiscItem = component.MiscItems.find(x => x.ItemType == ingredient.ItemType); const componentMiscItem = component.MiscItems.find(x => x.ItemType == ingredient.ItemType);
if (!componentMiscItem || componentMiscItem.ItemCount < scaleRequiredCount(ingredient.ItemCount)) { if (
!componentMiscItem ||
componentMiscItem.ItemCount < scaleRequiredCount(guild.Tier, ingredient.ItemCount)
) {
fullyFunded = false; fullyFunded = false;
break; break;
} }

View File

@ -1,56 +1,104 @@
import { GuildMember } from "@/src/models/guildModel"; import {
import { getGuildForRequestEx } from "@/src/services/guildService"; Alliance,
import { addFusionTreasures, addMiscItems, addShipDecorations, getInventory } from "@/src/services/inventoryService"; Guild,
GuildMember,
TGuildDatabaseDocument,
TGuildMemberDatabaseDocument
} from "@/src/models/guildModel";
import {
addGuildMemberMiscItemContribution,
addGuildMemberShipDecoContribution,
addVaultFusionTreasures,
addVaultMiscItems,
addVaultShipDecos,
getGuildForRequestEx
} from "@/src/services/guildService";
import {
addFusionTreasures,
addMiscItems,
addShipDecorations,
getInventory,
updateCurrency
} from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { IFusionTreasure, IMiscItem, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes"; import { IFusionTreasure, IMiscItem, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const contributeToVaultController: RequestHandler = async (req, res) => { export const contributeToVaultController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId, "GuildId RegularCredits MiscItems ShipDecorations FusionTreasures");
const request = JSON.parse(String(req.body)) as IContributeToVaultRequest;
if (request.Alliance) {
const guild = await getGuildForRequestEx(req, inventory); const guild = await getGuildForRequestEx(req, inventory);
const guildMember = (await GuildMember.findOne( const alliance = (await Alliance.findById(guild.AllianceId!))!;
alliance.VaultRegularCredits ??= 0;
alliance.VaultRegularCredits += request.RegularCredits;
if (request.FromVault) {
guild.VaultRegularCredits! -= request.RegularCredits;
await Promise.all([guild.save(), alliance.save()]);
} else {
updateCurrency(inventory, request.RegularCredits, false);
await Promise.all([inventory.save(), alliance.save()]);
}
res.end();
return;
}
let guild: TGuildDatabaseDocument;
let guildMember: TGuildMemberDatabaseDocument | undefined;
if (request.GuildVault) {
guild = (await Guild.findById(request.GuildVault))!;
} else {
guild = await getGuildForRequestEx(req, inventory);
guildMember = (await GuildMember.findOne(
{ accountId, guildId: guild._id }, { accountId, guildId: guild._id },
"RegularCreditsContributed MiscItemsContributed ShipDecorationsContributed" "RegularCreditsContributed MiscItemsContributed ShipDecorationsContributed"
))!; ))!;
const request = JSON.parse(String(req.body)) as IContributeToVaultRequest; }
if (request.RegularCredits) { if (request.RegularCredits) {
updateCurrency(inventory, request.RegularCredits, false);
guild.VaultRegularCredits ??= 0; guild.VaultRegularCredits ??= 0;
guild.VaultRegularCredits += request.RegularCredits; guild.VaultRegularCredits += request.RegularCredits;
if (guildMember) {
guildMember.RegularCreditsContributed ??= 0; guildMember.RegularCreditsContributed ??= 0;
guildMember.RegularCreditsContributed += request.RegularCredits; guildMember.RegularCreditsContributed += request.RegularCredits;
} }
}
if (request.MiscItems.length) { if (request.MiscItems.length) {
guild.VaultMiscItems ??= []; addVaultMiscItems(guild, request.MiscItems);
guildMember.MiscItemsContributed ??= [];
for (const item of request.MiscItems) { for (const item of request.MiscItems) {
guild.VaultMiscItems.push(item); if (guildMember) {
guildMember.MiscItemsContributed.push(item); addGuildMemberMiscItemContribution(guildMember, item);
}
addMiscItems(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]); addMiscItems(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]);
} }
} }
if (request.ShipDecorations.length) { if (request.ShipDecorations.length) {
guild.VaultShipDecorations ??= []; addVaultShipDecos(guild, request.ShipDecorations);
guildMember.ShipDecorationsContributed ??= [];
for (const item of request.ShipDecorations) { for (const item of request.ShipDecorations) {
guild.VaultShipDecorations.push(item); if (guildMember) {
guildMember.ShipDecorationsContributed.push(item); addGuildMemberShipDecoContribution(guildMember, item);
}
addShipDecorations(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]); addShipDecorations(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]);
} }
} }
if (request.FusionTreasures.length) { if (request.FusionTreasures.length) {
guild.VaultFusionTreasures ??= []; addVaultFusionTreasures(guild, request.FusionTreasures);
for (const item of request.FusionTreasures) { for (const item of request.FusionTreasures) {
guild.VaultFusionTreasures.push(item);
addFusionTreasures(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]); addFusionTreasures(inventory, [{ ...item, ItemCount: item.ItemCount * -1 }]);
} }
} }
await guild.save(); const promises: Promise<unknown>[] = [guild.save(), inventory.save()];
await inventory.save(); if (guildMember) {
await guildMember.save(); promises.push(guildMember.save());
}
await Promise.all(promises);
res.end(); res.end();
}; };
@ -59,4 +107,7 @@ interface IContributeToVaultRequest {
MiscItems: IMiscItem[]; MiscItems: IMiscItem[];
ShipDecorations: ITypeCount[]; ShipDecorations: ITypeCount[];
FusionTreasures: IFusionTreasure[]; FusionTreasures: IFusionTreasure[];
Alliance?: boolean;
FromVault?: boolean;
GuildVault?: string;
} }

View File

@ -0,0 +1,50 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel";
import { getAllianceClient } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
export const createAllianceController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId");
const guild = (await Guild.findById(inventory.GuildId!, "Name Tier AllianceId"))!;
if (guild.AllianceId) {
res.status(400).send("Guild is already in an alliance").end();
return;
}
const guildMember = (await GuildMember.findOne({ guildId: guild._id, accountId }, "rank"))!;
if (guildMember.rank > 1) {
res.status(400).send("Invalid permission").end();
return;
}
const data = getJSONfromString<ICreateAllianceRequest>(String(req.body));
const alliance = new Alliance({ Name: data.allianceName });
try {
await alliance.save();
} catch (e) {
res.status(400).send("Alliance name already in use").end();
return;
}
guild.AllianceId = alliance._id;
await Promise.all([
guild.save(),
AllianceMember.insertOne({
allianceId: alliance._id,
guildId: guild._id,
Pending: false,
Permissions:
GuildPermission.Ruler |
GuildPermission.Promoter |
GuildPermission.Recruiter |
GuildPermission.Treasurer |
GuildPermission.ChatModerator
})
]);
res.json(await getAllianceClient(alliance, guild));
};
interface ICreateAllianceRequest {
allianceName: string;
}

View File

@ -2,16 +2,16 @@ import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Guild, GuildMember } from "@/src/models/guildModel"; import { Guild, GuildMember } from "@/src/models/guildModel";
import { import { createUniqueClanName, getGuildClient } from "@/src/services/guildService";
createUniqueClanName, import { addRecipes, getInventory } from "@/src/services/inventoryService";
getGuildClient,
updateInventoryForConfirmedGuildJoin
} from "@/src/services/guildService";
export const createGuildController: RequestHandler = async (req, res) => { export const createGuildController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const payload = getJSONfromString<ICreateGuildRequest>(String(req.body)); const payload = getJSONfromString<ICreateGuildRequest>(String(req.body));
// Remove pending applications for this account
await GuildMember.deleteMany({ accountId, status: 1 });
// Create guild on database // Create guild on database
const guild = new Guild({ const guild = new Guild({
Name: await createUniqueClanName(payload.guildName) Name: await createUniqueClanName(payload.guildName)
@ -26,7 +26,15 @@ export const createGuildController: RequestHandler = async (req, res) => {
rank: 0 rank: 0
}); });
await updateInventoryForConfirmedGuildJoin(accountId, guild._id); const inventory = await getInventory(accountId, "GuildId Recipes");
inventory.GuildId = guild._id;
addRecipes(inventory, [
{
ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint",
ItemCount: 1
}
]);
await inventory.save();
res.json({ res.json({
...(await getGuildClient(guild, accountId)), ...(await getGuildClient(guild, accountId)),

View File

@ -0,0 +1,28 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { ICrewMemberClient } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
import { Types } from "mongoose";
export const crewMembersController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "CrewMembers");
const data = getJSONfromString<ICrewMembersRequest>(String(req.body));
const dbCrewMember = inventory.CrewMembers.id(data.crewMember.ItemId.$oid)!;
dbCrewMember.AssignedRole = data.crewMember.AssignedRole;
dbCrewMember.SkillEfficiency = data.crewMember.SkillEfficiency;
dbCrewMember.WeaponConfigIdx = data.crewMember.WeaponConfigIdx;
dbCrewMember.WeaponId = new Types.ObjectId(data.crewMember.WeaponId.$oid);
dbCrewMember.Configs = data.crewMember.Configs;
dbCrewMember.SecondInCommand = data.crewMember.SecondInCommand;
await inventory.save();
res.json({
crewMemberId: data.crewMember.ItemId.$oid,
NemesisFingerprint: data.crewMember.NemesisFingerprint
});
};
interface ICrewMembersRequest {
crewMember: ICrewMemberClient;
}

View File

@ -0,0 +1,91 @@
import {
addCrewShipSalvagedWeaponSkin,
addCrewShipRawSalvage,
getInventory,
addEquipment
} from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
import { ICrewShipComponentFingerprint, IInnateDamageFingerprint } from "@/src/types/inventoryTypes/inventoryTypes";
import { ExportCustoms, ExportRailjackWeapons, ExportUpgrades } from "warframe-public-export-plus";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { getRandomInt } from "@/src/services/rngService";
import { IFingerprintStat } from "@/src/helpers/rivenHelper";
import { IEquipmentDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes";
export const crewShipIdentifySalvageController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(
accountId,
"CrewShipSalvagedWeaponSkins CrewShipSalvagedWeapons CrewShipRawSalvage"
);
const payload = getJSONfromString<ICrewShipIdentifySalvageRequest>(String(req.body));
const inventoryChanges: IInventoryChanges = {};
if (payload.ItemType in ExportCustoms) {
const meta = ExportCustoms[payload.ItemType];
let upgradeFingerprint: ICrewShipComponentFingerprint = { compat: payload.ItemType, buffs: [] };
if (meta.subroutines) {
upgradeFingerprint = {
SubroutineIndex: getRandomInt(0, meta.subroutines.length - 1),
...upgradeFingerprint
};
}
for (const upgrade of meta.randomisedUpgrades!) {
upgradeFingerprint.buffs.push({ Tag: upgrade.tag, Value: Math.trunc(Math.random() * 0x40000000) });
}
addCrewShipSalvagedWeaponSkin(
inventory,
payload.ItemType,
JSON.stringify(upgradeFingerprint),
inventoryChanges
);
} else {
const meta = ExportRailjackWeapons[payload.ItemType];
let defaultOverwrites: Partial<IEquipmentDatabase> | undefined;
if (meta.defaultUpgrades?.[0]) {
const upgradeType = meta.defaultUpgrades[0].ItemType;
const upgradeMeta = ExportUpgrades[upgradeType];
const buffs: IFingerprintStat[] = [];
for (const buff of upgradeMeta.upgradeEntries!) {
buffs.push({
Tag: buff.tag,
Value: Math.trunc(Math.random() * 0x40000000)
});
}
defaultOverwrites = {
UpgradeType: upgradeType,
UpgradeFingerprint: JSON.stringify({
compat: payload.ItemType,
buffs
} satisfies IInnateDamageFingerprint)
};
}
addEquipment(
inventory,
"CrewShipSalvagedWeapons",
payload.ItemType,
undefined,
inventoryChanges,
defaultOverwrites
);
}
inventoryChanges.CrewShipRawSalvage = [
{
ItemType: payload.ItemType,
ItemCount: -1
}
];
addCrewShipRawSalvage(inventory, inventoryChanges.CrewShipRawSalvage);
await inventory.save();
res.json({
InventoryChanges: inventoryChanges
});
};
interface ICrewShipIdentifySalvageRequest {
ItemType: string;
}

View File

@ -0,0 +1,17 @@
import { AllianceMember, GuildMember } from "@/src/models/guildModel";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const declineAllianceInviteController: RequestHandler = async (req, res) => {
// Check requester is a warlord in their guild
const accountId = await getAccountIdForRequest(req);
const guildMember = (await GuildMember.findOne({ accountId, status: 0 }))!;
if (guildMember.rank > 1) {
res.status(400).json({ Error: 104 });
return;
}
await AllianceMember.deleteOne({ allianceId: req.query.allianceId, guildId: guildMember.guildId });
res.end();
};

View File

@ -0,0 +1,67 @@
import { Alliance, AllianceMember, Guild, GuildMember } from "@/src/models/guildModel";
import { getAccountForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { parallelForeach } from "@/src/utils/async-utils";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express";
export const divvyAllianceVaultController: RequestHandler = async (req, res) => {
// Afaict, there's no way to put anything other than credits in the alliance vault (anymore?), so just no-op if this is not a request to divvy credits.
if (req.query.credits == "1") {
// Check requester is a warlord in their guild
const account = await getAccountForRequest(req);
const guildMember = (await GuildMember.findOne({ accountId: account._id, status: 0 }))!;
if (guildMember.rank > 1) {
res.status(400).end();
return;
}
// Check guild has treasurer permissions in the alliance
const allianceMember = (await AllianceMember.findOne({
allianceId: req.query.allianceId,
guildId: guildMember.guildId
}))!;
if (!(allianceMember.Permissions & GuildPermission.Treasurer)) {
res.status(400).end();
return;
}
const allianceMembers = await AllianceMember.find({ allianceId: req.query.allianceId });
const memberCounts: Record<string, number> = {};
let totalMembers = 0;
await parallelForeach(allianceMembers, async allianceMember => {
const memberCount = await GuildMember.countDocuments({
guildId: allianceMember.guildId
});
memberCounts[allianceMember.guildId.toString()] = memberCount;
totalMembers += memberCount;
});
logger.debug(`alliance has ${totalMembers} members between all its clans`);
const alliance = (await Alliance.findById(allianceMember.allianceId, "VaultRegularCredits"))!;
if (alliance.VaultRegularCredits) {
let creditsHandedOutInTotal = 0;
await parallelForeach(allianceMembers, async allianceMember => {
const memberCount = memberCounts[allianceMember.guildId.toString()];
const cutPercentage = memberCount / totalMembers;
const creditsToHandOut = Math.trunc(alliance.VaultRegularCredits! * cutPercentage);
logger.debug(
`${allianceMember.guildId.toString()} has ${memberCount} member(s) = ${Math.trunc(cutPercentage * 100)}% of alliance; giving ${creditsToHandOut} credit(s)`
);
if (creditsToHandOut != 0) {
await Guild.updateOne(
{ _id: allianceMember.guildId },
{ $inc: { VaultRegularCredits: creditsToHandOut } }
);
creditsHandedOutInTotal += creditsToHandOut;
}
});
alliance.VaultRegularCredits -= creditsHandedOutInTotal;
logger.debug(
`handed out ${creditsHandedOutInTotal} credits; alliance vault now has ${alliance.VaultRegularCredits} credit(s)`
);
}
await alliance.save();
}
res.end();
};

View File

@ -1,4 +1,4 @@
import { GuildMember } from "@/src/models/guildModel"; import { GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel";
import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, scaleRequiredCount } from "@/src/services/guildService"; import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, scaleRequiredCount } from "@/src/services/guildService";
import { getInventory, updateCurrency } from "@/src/services/inventoryService"; import { getInventory, updateCurrency } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
@ -36,10 +36,10 @@ export const dojoComponentRushController: RequestHandler = async (req, res) => {
if (request.DecoId) { if (request.DecoId) {
const deco = component.Decos!.find(x => x._id.equals(request.DecoId))!; const deco = component.Decos!.find(x => x._id.equals(request.DecoId))!;
const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == deco.Type)!; const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == deco.Type)!;
processContribution(deco, meta, platinumDonated); processContribution(guild, deco, meta, platinumDonated);
} else { } else {
const meta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf)!; const meta = Object.values(ExportDojoRecipes.rooms).find(x => x.resultType == component.pf)!;
processContribution(component, meta, platinumDonated); processContribution(guild, component, meta, platinumDonated);
const entry = guild.RoomChanges?.find(x => x.componentId.equals(component._id)); const entry = guild.RoomChanges?.find(x => x.componentId.equals(component._id));
if (entry) { if (entry) {
@ -47,13 +47,11 @@ export const dojoComponentRushController: RequestHandler = async (req, res) => {
} }
} }
await guild.save();
await inventory.save();
const guildMember = (await GuildMember.findOne({ accountId, guildId: guild._id }, "PremiumCreditsContributed"))!; const guildMember = (await GuildMember.findOne({ accountId, guildId: guild._id }, "PremiumCreditsContributed"))!;
guildMember.PremiumCreditsContributed ??= 0; guildMember.PremiumCreditsContributed ??= 0;
guildMember.PremiumCreditsContributed += request.Amount; guildMember.PremiumCreditsContributed += request.Amount;
await guildMember.save();
await Promise.all([guild.save(), inventory.save(), guildMember.save()]);
res.json({ res.json({
...(await getDojoClient(guild, 0, component._id)), ...(await getDojoClient(guild, 0, component._id)),
@ -61,8 +59,13 @@ export const dojoComponentRushController: RequestHandler = async (req, res) => {
}); });
}; };
const processContribution = (component: IDojoContributable, meta: IDojoBuild, platinumDonated: number): void => { const processContribution = (
const fullPlatinumCost = scaleRequiredCount(meta.skipTimePrice); guild: TGuildDatabaseDocument,
component: IDojoContributable,
meta: IDojoBuild,
platinumDonated: number
): void => {
const fullPlatinumCost = scaleRequiredCount(guild.Tier, meta.skipTimePrice);
const fullDurationSeconds = meta.time; const fullDurationSeconds = meta.time;
const secondsPerPlatinum = fullDurationSeconds / fullPlatinumCost; const secondsPerPlatinum = fullDurationSeconds / fullPlatinumCost;
component.CompletionTime = new Date( component.CompletionTime = new Date(

View File

@ -1,5 +1,11 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
// Arbiter Dojo endpoints, not really used by us as we don't provide a ContentURL.
export const dojoController: RequestHandler = (_req, res) => { export const dojoController: RequestHandler = (_req, res) => {
res.json("-1"); // Tell client to use authorised request. res.json("-1"); // Tell client to use authorised request.
}; };
export const setDojoURLController: RequestHandler = (_req, res) => {
res.end();
};

View File

@ -55,7 +55,7 @@ export const dronesController: RequestHandler = async (req, res) => {
? new Date() ? new Date()
: new Date(Date.now() + getRandomInt(3 * 3600 * 1000, 4 * 3600 * 1000)); : new Date(Date.now() + getRandomInt(3 * 3600 * 1000, 4 * 3600 * 1000));
drone.PendingDamage = drone.PendingDamage =
Math.random() < system.damageChance !config.noResourceExtractorDronesDamage && Math.random() < system.damageChance
? getRandomInt(system.droneDamage.minValue, system.droneDamage.maxValue) ? getRandomInt(system.droneDamage.minValue, system.droneDamage.maxValue)
: 0; : 0;
const resource = getRandomWeightedRewardUc(system.resources, droneMeta.probabilities)!; const resource = getRandomWeightedRewardUc(system.resources, droneMeta.probabilities)!;

View File

@ -17,7 +17,7 @@ export const evolveWeaponController: RequestHandler = async (req, res) => {
recipe.ingredients.map(x => ({ ItemType: x.ItemType, ItemCount: x.ItemCount * -1 })) recipe.ingredients.map(x => ({ ItemType: x.ItemType, ItemCount: x.ItemCount * -1 }))
); );
const item = inventory[payload.Category].find(item => item._id.toString() == (req.query.ItemId as string))!; const item = inventory[payload.Category].id(req.query.ItemId as string)!;
item.Features ??= 0; item.Features ??= 0;
item.Features |= EquipmentFeatures.INCARNON_GENESIS; item.Features |= EquipmentFeatures.INCARNON_GENESIS;
@ -39,7 +39,7 @@ export const evolveWeaponController: RequestHandler = async (req, res) => {
} }
]); ]);
const item = inventory[payload.Category].find(item => item._id.toString() == (req.query.ItemId as string))!; const item = inventory[payload.Category].id(req.query.ItemId as string)!;
item.Features! &= ~EquipmentFeatures.INCARNON_GENESIS; item.Features! &= ~EquipmentFeatures.INCARNON_GENESIS;
} else { } else {
throw new Error(`unexpected evolve weapon action: ${payload.Action}`); throw new Error(`unexpected evolve weapon action: ${payload.Action}`);

View File

@ -1,10 +1,9 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper"; import { addMiscItems, addStanding, getInventory } from "@/src/services/inventoryService";
import { addMiscItems, getInventory, getStandingLimit, updateStandingLimit } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { ExportResources, ExportSyndicates } from "warframe-public-export-plus"; import { ExportResources } from "warframe-public-export-plus";
export const fishmongerController: RequestHandler = async (req, res) => { export const fishmongerController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
@ -31,32 +30,15 @@ export const fishmongerController: RequestHandler = async (req, res) => {
miscItemChanges.push({ ItemType: fish.ItemType, ItemCount: fish.ItemCount * -1 }); miscItemChanges.push({ ItemType: fish.ItemType, ItemCount: fish.ItemCount * -1 });
} }
addMiscItems(inventory, miscItemChanges); addMiscItems(inventory, miscItemChanges);
if (gainedStanding && syndicateTag) { let affiliationMod;
let syndicate = inventory.Affiliations.find(x => x.Tag == syndicateTag); if (gainedStanding && syndicateTag) affiliationMod = addStanding(inventory, syndicateTag, gainedStanding);
if (!syndicate) {
syndicate = inventory.Affiliations[inventory.Affiliations.push({ Tag: syndicateTag, Standing: 0 }) - 1];
}
const syndicateMeta = ExportSyndicates[syndicateTag];
const max = getMaxStanding(syndicateMeta, syndicate.Title ?? 0);
if (syndicate.Standing + gainedStanding > max) {
gainedStanding = max - syndicate.Standing;
}
if (gainedStanding > getStandingLimit(inventory, syndicateMeta.dailyLimitBin)) {
gainedStanding = getStandingLimit(inventory, syndicateMeta.dailyLimitBin);
}
syndicate.Standing += gainedStanding;
updateStandingLimit(inventory, syndicateMeta.dailyLimitBin, gainedStanding);
}
await inventory.save(); await inventory.save();
res.json({ res.json({
InventoryChanges: { InventoryChanges: {
MiscItems: miscItemChanges MiscItems: miscItemChanges
}, },
SyndicateTag: syndicateTag, SyndicateTag: syndicateTag,
StandingChange: gainedStanding StandingChange: affiliationMod?.Standing || 0
}); });
}; };

View File

@ -18,8 +18,8 @@ export const focusController: RequestHandler = async (req, res) => {
case FocusOperation.InstallLens: { case FocusOperation.InstallLens: {
const request = JSON.parse(String(req.body)) as ILensInstallRequest; const request = JSON.parse(String(req.body)) as ILensInstallRequest;
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
for (const item of inventory[request.Category]) { const item = inventory[request.Category].id(request.WeaponId);
if (item._id.toString() == request.WeaponId) { if (item) {
item.FocusLens = request.LensType; item.FocusLens = request.LensType;
addMiscItems(inventory, [ addMiscItems(inventory, [
{ {
@ -27,8 +27,6 @@ export const focusController: RequestHandler = async (req, res) => {
ItemCount: -1 ItemCount: -1
} satisfies IMiscItem } satisfies IMiscItem
]); ]);
break;
}
} }
await inventory.save(); await inventory.save();
res.json({ res.json({

View File

@ -1,7 +1,25 @@
import { Alliance, Guild } from "@/src/models/guildModel";
import { getAllianceClient } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
const getAllianceController: RequestHandler = (_req, res) => { export const getAllianceController: RequestHandler = async (req, res) => {
res.sendStatus(200); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId");
if (inventory.GuildId) {
const guild = (await Guild.findById(inventory.GuildId, "Name Tier AllianceId"))!;
if (guild.AllianceId) {
const alliance = (await Alliance.findById(guild.AllianceId))!;
res.json(await getAllianceClient(alliance, guild));
return;
}
}
res.end();
}; };
export { getAllianceController }; /*interface IGetAllianceRequest {
memberCount: number;
clanLeaderName: string;
clanLeaderId: string;
}*/

View File

@ -1,6 +1,7 @@
import { GuildMember } from "@/src/models/guildModel"; import { GuildMember } from "@/src/models/guildModel";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { IGuildMemberClient } from "@/src/types/guildTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const getGuildContributionsController: RequestHandler = async (req, res) => { export const getGuildContributionsController: RequestHandler = async (req, res) => {
@ -8,11 +9,11 @@ export const getGuildContributionsController: RequestHandler = async (req, res)
const guildId = (await getInventory(accountId, "GuildId")).GuildId; const guildId = (await getInventory(accountId, "GuildId")).GuildId;
const guildMember = (await GuildMember.findOne({ guildId, accountId: req.query.buddyId }))!; const guildMember = (await GuildMember.findOne({ guildId, accountId: req.query.buddyId }))!;
res.json({ res.json({
_id: { $oid: req.query.buddyId }, _id: { $oid: req.query.buddyId as string },
RegularCreditsContributed: guildMember.RegularCreditsContributed, RegularCreditsContributed: guildMember.RegularCreditsContributed,
PremiumCreditsContributed: guildMember.PremiumCreditsContributed, PremiumCreditsContributed: guildMember.PremiumCreditsContributed,
MiscItemsContributed: guildMember.MiscItemsContributed, MiscItemsContributed: guildMember.MiscItemsContributed,
ConsumablesContributed: [], // ??? ConsumablesContributed: [], // ???
ShipDecorationsContributed: guildMember.ShipDecorationsContributed ShipDecorationsContributed: guildMember.ShipDecorationsContributed
}); } satisfies Partial<IGuildMemberClient>);
}; };

View File

@ -5,7 +5,7 @@ import { logger } from "@/src/utils/logger";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { createUniqueClanName, getGuildClient } from "@/src/services/guildService"; import { createUniqueClanName, getGuildClient } from "@/src/services/guildService";
const getGuildController: RequestHandler = async (req, res) => { export const getGuildController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId"); const inventory = await getInventory(accountId, "GuildId");
if (inventory.GuildId) { if (inventory.GuildId) {
@ -28,7 +28,5 @@ const getGuildController: RequestHandler = async (req, res) => {
return; return;
} }
} }
res.sendStatus(200); res.end();
}; };
export { getGuildController };

View File

@ -1,16 +1,20 @@
import { toOid } from "@/src/helpers/inventoryHelpers";
import { Account, Ignore } from "@/src/models/loginModel";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IFriendInfo } from "@/src/types/guildTypes";
import { parallelForeach } from "@/src/utils/async-utils";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
const getIgnoredUsersController: RequestHandler = (_req, res) => { export const getIgnoredUsersController: RequestHandler = async (req, res) => {
res.writeHead(200, { const accountId = await getAccountIdForRequest(req);
"Content-Type": "text/html", const ignores = await Ignore.find({ ignorer: accountId });
"Content-Length": "3" const ignoredUsers: IFriendInfo[] = [];
await parallelForeach(ignores, async ignore => {
const ignoreeAccount = (await Account.findById(ignore.ignoree, "DisplayName"))!;
ignoredUsers.push({
_id: toOid(ignore.ignoree),
DisplayName: ignoreeAccount.DisplayName + ""
}); });
res.end( });
Buffer.from([ res.json({ IgnoredUsers: ignoredUsers });
0x7b, 0x22, 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x22, 0x3a, 0x38, 0x33, 0x30, 0x34, 0x30, 0x37, 0x37, 0x32, 0x32,
0x34, 0x30, 0x32, 0x32, 0x32, 0x36, 0x31, 0x35, 0x30, 0x31, 0x7d
])
);
}; };
export { getIgnoredUsersController };

View File

@ -1,13 +1,12 @@
import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
import { generateRewardSeed } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const getNewRewardSeedController: RequestHandler = async (req, res) => { export const getNewRewardSeedController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const rewardSeed = generateRewardSeed(); const rewardSeed = generateRewardSeed();
logger.debug(`generated new reward seed: ${rewardSeed}`);
await Inventory.updateOne( await Inventory.updateOne(
{ {
accountOwnerId: accountId accountOwnerId: accountId
@ -18,9 +17,3 @@ export const getNewRewardSeedController: RequestHandler = async (req, res) => {
); );
res.json({ rewardSeed: rewardSeed }); res.json({ rewardSeed: rewardSeed });
}; };
export function generateRewardSeed(): number {
const min = -Number.MAX_SAFE_INTEGER;
const max = Number.MAX_SAFE_INTEGER;
return Math.floor(Math.random() * (max - min + 1)) + min;
}

View File

@ -26,7 +26,10 @@ export const getShipController: RequestHandler = async (req, res) => {
Colors: personalRooms.ShipInteriorColors, Colors: personalRooms.ShipInteriorColors,
ShipAttachments: ship.ShipAttachments, ShipAttachments: ship.ShipAttachments,
SkinFlavourItem: ship.SkinFlavourItem SkinFlavourItem: ship.SkinFlavourItem
} },
FavouriteLoadoutId: personalRooms.Ship.FavouriteLoadoutId
? toOid(personalRooms.Ship.FavouriteLoadoutId)
: undefined
}, },
Apartment: personalRooms.Apartment, Apartment: personalRooms.Apartment,
TailorShop: personalRooms.TailorShop TailorShop: personalRooms.TailorShop

View File

@ -1,5 +1,5 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getVendorManifestByTypeName, preprocessVendorManifest } from "@/src/services/serversideVendorsService"; import { getVendorManifestByTypeName } from "@/src/services/serversideVendorsService";
export const getVendorInfoController: RequestHandler = (req, res) => { export const getVendorInfoController: RequestHandler = (req, res) => {
if (typeof req.query.vendor == "string") { if (typeof req.query.vendor == "string") {
@ -7,7 +7,7 @@ export const getVendorInfoController: RequestHandler = (req, res) => {
if (!manifest) { if (!manifest) {
throw new Error(`Unknown vendor: ${req.query.vendor}`); throw new Error(`Unknown vendor: ${req.query.vendor}`);
} }
res.json(preprocessVendorManifest(manifest)); res.json(manifest);
} else { } else {
res.status(400).end(); res.status(400).end();
} }

View File

@ -56,7 +56,7 @@ export const giftingController: RequestHandler = async (req, res) => {
await senderInventory.save(); await senderInventory.save();
const senderName = getSuffixedName(senderAccount); const senderName = getSuffixedName(senderAccount);
await createMessage(account._id.toString(), [ await createMessage(account._id, [
{ {
sndr: senderName, sndr: senderName,
msg: data.Message || "/Lotus/Language/Menu/GiftReceivedBody_NoCustomMessage", msg: data.Message || "/Lotus/Language/Menu/GiftReceivedBody_NoCustomMessage",

View File

@ -2,36 +2,25 @@ import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addMiscItems, getInventory } from "@/src/services/inventoryService"; import { addMiscItems, getInventory } from "@/src/services/inventoryService";
import { WeaponTypeInternal } from "@/src/services/itemDataService"; import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { ArtifactPolarity, EquipmentFeatures, IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { ArtifactPolarity, EquipmentFeatures, IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { ExportRecipes } from "warframe-public-export-plus"; import { ExportRecipes } from "warframe-public-export-plus";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
const modularWeaponCategory: (WeaponTypeInternal | "Hoverboards")[] = [
"LongGuns",
"Pistols",
"Melee",
"OperatorAmps",
"Hoverboards"
];
interface IGildWeaponRequest { interface IGildWeaponRequest {
ItemName: string; ItemName: string;
Recipe: string; // e.g. /Lotus/Weapons/SolarisUnited/LotusGildKitgunBlueprint Recipe: string; // e.g. /Lotus/Weapons/SolarisUnited/LotusGildKitgunBlueprint
PolarizeSlot?: number; PolarizeSlot?: number;
PolarizeValue?: ArtifactPolarity; PolarizeValue?: ArtifactPolarity;
ItemId: string; ItemId: string;
Category: WeaponTypeInternal | "Hoverboards"; Category: TEquipmentKey;
} }
export const gildWeaponController: RequestHandler = async (req, res) => { export const gildWeaponController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const data = getJSONfromString<IGildWeaponRequest>(String(req.body)); const data = getJSONfromString<IGildWeaponRequest>(String(req.body));
data.ItemId = String(req.query.ItemId); data.ItemId = String(req.query.ItemId);
if (!modularWeaponCategory.includes(req.query.Category as WeaponTypeInternal | "Hoverboards")) { data.Category = req.query.Category as TEquipmentKey;
throw new Error(`Unknown modular weapon Category: ${String(req.query.Category)}`);
}
data.Category = req.query.Category as WeaponTypeInternal | "Hoverboards";
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const weaponIndex = inventory[data.Category].findIndex(x => String(x._id) === data.ItemId); const weaponIndex = inventory[data.Category].findIndex(x => String(x._id) === data.ItemId);
@ -42,8 +31,10 @@ export const gildWeaponController: RequestHandler = async (req, res) => {
const weapon = inventory[data.Category][weaponIndex]; const weapon = inventory[data.Category][weaponIndex];
weapon.Features ??= 0; weapon.Features ??= 0;
weapon.Features |= EquipmentFeatures.GILDED; weapon.Features |= EquipmentFeatures.GILDED;
if (data.Recipe != "webui") {
weapon.ItemName = data.ItemName; weapon.ItemName = data.ItemName;
weapon.XP = 0; weapon.XP = 0;
}
if (data.Category != "OperatorAmps" && data.PolarizeSlot && data.PolarizeValue) { if (data.Category != "OperatorAmps" && data.PolarizeSlot && data.PolarizeValue) {
weapon.Polarity = [ weapon.Polarity = [
{ {
@ -56,6 +47,9 @@ export const gildWeaponController: RequestHandler = async (req, res) => {
const inventoryChanges: IInventoryChanges = {}; const inventoryChanges: IInventoryChanges = {};
inventoryChanges[data.Category] = [weapon.toJSON<IEquipmentClient>()]; inventoryChanges[data.Category] = [weapon.toJSON<IEquipmentClient>()];
const affiliationMods = [];
if (data.Recipe != "webui") {
const recipe = ExportRecipes[data.Recipe]; const recipe = ExportRecipes[data.Recipe];
inventoryChanges.MiscItems = recipe.secretIngredients!.map(ingredient => ({ inventoryChanges.MiscItems = recipe.secretIngredients!.map(ingredient => ({
ItemType: ingredient.ItemType, ItemType: ingredient.ItemType,
@ -63,7 +57,6 @@ export const gildWeaponController: RequestHandler = async (req, res) => {
})); }));
addMiscItems(inventory, inventoryChanges.MiscItems); addMiscItems(inventory, inventoryChanges.MiscItems);
const affiliationMods = [];
if (recipe.syndicateStandingChange) { if (recipe.syndicateStandingChange) {
const affiliation = inventory.Affiliations.find(x => x.Tag == recipe.syndicateStandingChange!.tag)!; const affiliation = inventory.Affiliations.find(x => x.Tag == recipe.syndicateStandingChange!.tag)!;
affiliation.Standing += recipe.syndicateStandingChange.value; affiliation.Standing += recipe.syndicateStandingChange.value;
@ -72,6 +65,7 @@ export const gildWeaponController: RequestHandler = async (req, res) => {
Standing: recipe.syndicateStandingChange.value Standing: recipe.syndicateStandingChange.value
}); });
} }
}
await inventory.save(); await inventory.save();
res.json({ res.json({

View File

@ -2,8 +2,8 @@ import { RequestHandler } from "express";
import { parseString } from "@/src/helpers/general"; import { parseString } from "@/src/helpers/general";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { IGroup } from "@/src/types/loginTypes";
import { giveKeyChainItem } from "@/src/services/questService"; import { giveKeyChainItem } from "@/src/services/questService";
import { IKeyChainRequest } from "@/src/types/requestTypes";
export const giveKeyChainTriggeredItemsController: RequestHandler = async (req, res) => { export const giveKeyChainTriggeredItemsController: RequestHandler = async (req, res) => {
const accountId = parseString(req.query.accountId); const accountId = parseString(req.query.accountId);
@ -15,9 +15,3 @@ export const giveKeyChainTriggeredItemsController: RequestHandler = async (req,
res.send(inventoryChanges); res.send(inventoryChanges);
}; };
export interface IKeyChainRequest {
KeyChain: string;
ChainStage: number;
Groups?: IGroup[];
}

View File

@ -1,7 +1,7 @@
import { IKeyChainRequest } from "@/src/controllers/api/giveKeyChainTriggeredItemsController";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { giveKeyChainMessage } from "@/src/services/questService"; import { giveKeyChainMessage } from "@/src/services/questService";
import { IKeyChainRequest } from "@/src/types/requestTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const giveKeyChainTriggeredMessageController: RequestHandler = async (req, res) => { export const giveKeyChainTriggeredMessageController: RequestHandler = async (req, res) => {

View File

@ -16,15 +16,15 @@ export const giveQuestKeyRewardController: RequestHandler = async (req, res) =>
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const inventoryChanges = await addItem(inventory, reward.ItemType, reward.Amount); const inventoryChanges = await addItem(inventory, reward.ItemType, reward.Amount);
await inventory.save(); await inventory.save();
res.json(inventoryChanges.InventoryChanges); res.json(inventoryChanges);
//TODO: consider whishlist changes //TODO: consider whishlist changes
}; };
export interface IQuestKeyRewardRequest { interface IQuestKeyRewardRequest {
reward: IQuestKeyReward; reward: IQuestKeyReward;
} }
export interface IQuestKeyReward { interface IQuestKeyReward {
RewardType: string; RewardType: string;
CouponType: string; CouponType: string;
Icon: string; Icon: string;

View File

@ -0,0 +1,20 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addLoreFragmentScans, addShipDecorations, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { ILoreFragmentScan, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
export const giveShipDecoAndLoreFragmentController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "LoreFragmentScans ShipDecorations");
const data = getJSONfromString<IGiveShipDecoAndLoreFragmentRequest>(String(req.body));
addLoreFragmentScans(inventory, data.LoreFragmentScans);
addShipDecorations(inventory, data.ShipDecorations);
await inventory.save();
res.end();
};
interface IGiveShipDecoAndLoreFragmentRequest {
LoreFragmentScans: ILoreFragmentScan[];
ShipDecorations: ITypeCount[];
}

View File

@ -1,20 +1,8 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { InventoryDocumentProps } from "@/src/models/inventoryModels/inventoryModel"; import { addStartingGear, getInventory } from "@/src/services/inventoryService";
import {
addEquipment,
addItem,
addPowerSuit,
combineInventoryChanges,
getInventory,
updateSlots
} from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { IInventoryClient, IInventoryDatabase, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; import { TPartialStartingGear } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { HydratedDocument } from "mongoose";
type TPartialStartingGear = Pick<IInventoryClient, "LongGuns" | "Suits" | "Pistols" | "Melee">;
export const giveStartingGearController: RequestHandler = async (req, res) => { export const giveStartingGearController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
@ -26,72 +14,3 @@ export const giveStartingGearController: RequestHandler = async (req, res) => {
res.send(inventoryChanges); res.send(inventoryChanges);
}; };
//TODO: RawUpgrades might need to return a LastAdded
const awakeningRewards = [
"/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem1",
"/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem2",
"/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem3",
"/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem4",
"/Lotus/Types/Restoratives/LisetAutoHack",
"/Lotus/Upgrades/Mods/Warframe/AvatarShieldMaxMod"
];
export const addStartingGear = async (
inventory: HydratedDocument<IInventoryDatabase, InventoryDocumentProps>,
startingGear: TPartialStartingGear | undefined = undefined
): Promise<IInventoryChanges> => {
const { LongGuns, Pistols, Suits, Melee } = startingGear || {
LongGuns: [{ ItemType: "/Lotus/Weapons/Tenno/Rifle/Rifle" }],
Pistols: [{ ItemType: "/Lotus/Weapons/Tenno/Pistol/Pistol" }],
Suits: [{ ItemType: "/Lotus/Powersuits/Excalibur/Excalibur" }],
Melee: [{ ItemType: "/Lotus/Weapons/Tenno/Melee/LongSword/LongSword" }]
};
//TODO: properly merge weapon bin changes it is currently static here
const inventoryChanges: IInventoryChanges = {};
addEquipment(inventory, "LongGuns", LongGuns[0].ItemType, undefined, inventoryChanges);
addEquipment(inventory, "Pistols", Pistols[0].ItemType, undefined, inventoryChanges);
addEquipment(inventory, "Melee", Melee[0].ItemType, undefined, inventoryChanges);
addPowerSuit(inventory, Suits[0].ItemType, inventoryChanges);
addEquipment(
inventory,
"DataKnives",
"/Lotus/Weapons/Tenno/HackingDevices/TnHackingDevice/TnHackingDeviceWeapon",
undefined,
inventoryChanges,
{ XP: 450_000 }
);
addEquipment(
inventory,
"Scoops",
"/Lotus/Weapons/Tenno/Speedball/SpeedballWeaponTest",
undefined,
inventoryChanges
);
updateSlots(inventory, InventorySlot.SUITS, 0, 1);
updateSlots(inventory, InventorySlot.WEAPONS, 0, 3);
inventoryChanges.SuitBin = { count: 1, platinum: 0, Slots: -1 };
inventoryChanges.WeaponBin = { count: 3, platinum: 0, Slots: -3 };
await addItem(inventory, "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain");
inventory.ActiveQuest = "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain";
inventory.PremiumCredits = 50;
inventory.PremiumCreditsFree = 50;
inventoryChanges.PremiumCredits = 50;
inventoryChanges.PremiumCreditsFree = 50;
inventory.RegularCredits = 3000;
inventoryChanges.RegularCredits = 3000;
for (const item of awakeningRewards) {
const inventoryDelta = await addItem(inventory, item);
combineInventoryChanges(inventoryChanges, inventoryDelta);
}
inventory.PlayedParkourTutorial = true;
inventory.ReceivedStartingGear = true;
return inventoryChanges;
};

View File

@ -1,38 +1,46 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { import {
addGuildMemberMiscItemContribution,
getGuildForRequestEx, getGuildForRequestEx,
getGuildVault, getGuildVault,
hasAccessToDojo, hasAccessToDojo,
hasGuildPermission, hasGuildPermission,
processFundedGuildTechProject,
processGuildTechProjectContributionsUpdate,
removePigmentsFromGuildMembers, removePigmentsFromGuildMembers,
scaleRequiredCount scaleRequiredCount,
setGuildTechLogState
} from "@/src/services/guildService"; } from "@/src/services/guildService";
import { ExportDojoRecipes, IDojoResearch } from "warframe-public-export-plus"; import { ExportDojoRecipes } from "warframe-public-export-plus";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { import {
addCrewShipWeaponSkin,
addEquipment,
addItem, addItem,
addMiscItems, addMiscItems,
addRecipes, addRecipes,
combineInventoryChanges, combineInventoryChanges,
getInventory, getInventory,
occupySlot,
updateCurrency updateCurrency
} from "@/src/services/inventoryService"; } from "@/src/services/inventoryService";
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes"; import { IMiscItem, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { config } from "@/src/services/configService"; import { config } from "@/src/services/configService";
import { GuildPermission, ITechProjectClient, ITechProjectDatabase } from "@/src/types/guildTypes"; import { GuildPermission, ITechProjectClient } from "@/src/types/guildTypes";
import { GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel"; import { GuildMember } from "@/src/models/guildModel";
import { toMongoDate } from "@/src/helpers/inventoryHelpers"; import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
export const guildTechController: RequestHandler = async (req, res) => { export const guildTechController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const guild = await getGuildForRequestEx(req, inventory);
const data = JSON.parse(String(req.body)) as TGuildTechRequest; const data = JSON.parse(String(req.body)) as TGuildTechRequest;
if (data.Action == "Sync") { if (data.Action == "Sync") {
let needSave = false; let needSave = false;
const techProjects: ITechProjectClient[] = []; const techProjects: ITechProjectClient[] = [];
const guild = await getGuildForRequestEx(req, inventory);
if (guild.TechProjects) { if (guild.TechProjects) {
for (const project of guild.TechProjects) { for (const project of guild.TechProjects) {
const techProject: ITechProjectClient = { const techProject: ITechProjectClient = {
@ -44,7 +52,7 @@ export const guildTechController: RequestHandler = async (req, res) => {
if (project.CompletionDate) { if (project.CompletionDate) {
techProject.CompletionDate = toMongoDate(project.CompletionDate); techProject.CompletionDate = toMongoDate(project.CompletionDate);
if (Date.now() >= project.CompletionDate.getTime()) { if (Date.now() >= project.CompletionDate.getTime()) {
needSave ||= setTechLogState(guild, project.ItemType, 4, project.CompletionDate); needSave ||= setGuildTechLogState(guild, project.ItemType, 4, project.CompletionDate);
} }
} }
techProjects.push(techProject); techProjects.push(techProject);
@ -55,6 +63,8 @@ export const guildTechController: RequestHandler = async (req, res) => {
} }
res.json({ TechProjects: techProjects }); res.json({ TechProjects: techProjects });
} else if (data.Action == "Start") { } else if (data.Action == "Start") {
if (data.Mode == "Guild") {
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) { if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
res.status(400).send("-1").end(); res.status(400).send("-1").end();
return; return;
@ -66,17 +76,17 @@ export const guildTechController: RequestHandler = async (req, res) => {
guild.TechProjects[ guild.TechProjects[
guild.TechProjects.push({ guild.TechProjects.push({
ItemType: data.RecipeType, ItemType: data.RecipeType,
ReqCredits: config.noDojoResearchCosts ? 0 : scaleRequiredCount(recipe.price), ReqCredits: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, recipe.price),
ReqItems: recipe.ingredients.map(x => ({ ReqItems: recipe.ingredients.map(x => ({
ItemType: x.ItemType, ItemType: x.ItemType,
ItemCount: config.noDojoResearchCosts ? 0 : scaleRequiredCount(x.ItemCount) ItemCount: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, x.ItemCount)
})), })),
State: 0 State: 0
}) - 1 }) - 1
]; ];
setTechLogState(guild, techProject.ItemType, 5); setGuildTechLogState(guild, techProject.ItemType, 5);
if (config.noDojoResearchCosts) { if (config.noDojoResearchCosts) {
processFundedProject(guild, techProject, recipe); processFundedGuildTechProject(guild, techProject, recipe);
} else { } else {
if (data.RecipeType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/") { if (data.RecipeType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/") {
guild.ActiveDojoColorResearch = data.RecipeType; guild.ActiveDojoColorResearch = data.RecipeType;
@ -85,38 +95,109 @@ export const guildTechController: RequestHandler = async (req, res) => {
} }
await guild.save(); await guild.save();
res.end(); res.end();
} else {
const recipe = ExportDojoRecipes.research[data.RecipeType];
if (data.TechProductCategory) {
if (
data.TechProductCategory != "CrewShipWeapons" &&
data.TechProductCategory != "CrewShipWeaponSkins"
) {
throw new Error(`unexpected TechProductCategory: ${data.TechProductCategory}`);
}
if (!inventory[getSalvageCategory(data.TechProductCategory)].id(data.CategoryItemId)) {
throw new Error(
`no item with id ${data.CategoryItemId} in ${getSalvageCategory(data.TechProductCategory)} array`
);
}
}
const techProject =
inventory.PersonalTechProjects[
inventory.PersonalTechProjects.push({
State: 0,
ReqCredits: recipe.price,
ItemType: data.RecipeType,
ProductCategory: data.TechProductCategory,
CategoryItemId: data.CategoryItemId,
ReqItems: recipe.ingredients
}) - 1
];
await inventory.save();
res.json({
isPersonal: true,
action: "Start",
personalTech: techProject.toJSON()
});
}
} else if (data.Action == "Contribute") { } else if (data.Action == "Contribute") {
if ((req.query.guildId as string) == "000000000000000000000000") {
const techProject = inventory.PersonalTechProjects.id(data.ResearchId)!;
techProject.ReqCredits -= data.RegularCredits;
const inventoryChanges: IInventoryChanges = updateCurrency(inventory, data.RegularCredits, false);
const miscItemChanges = [];
for (const miscItem of data.MiscItems) {
const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType);
if (reqItem) {
if (miscItem.ItemCount > reqItem.ItemCount) {
miscItem.ItemCount = reqItem.ItemCount;
}
reqItem.ItemCount -= miscItem.ItemCount;
miscItemChanges.push({
ItemType: miscItem.ItemType,
ItemCount: miscItem.ItemCount * -1
});
}
}
addMiscItems(inventory, miscItemChanges);
inventoryChanges.MiscItems = miscItemChanges;
techProject.HasContributions = true;
if (techProject.ReqCredits == 0 && !techProject.ReqItems.find(x => x.ItemCount > 0)) {
techProject.State = 1;
const recipe = ExportDojoRecipes.research[techProject.ItemType];
techProject.CompletionDate = new Date(Date.now() + recipe.time * 1000);
}
await inventory.save();
res.json({
InventoryChanges: inventoryChanges,
PersonalResearch: { $oid: data.ResearchId },
PersonalResearchDate: techProject.CompletionDate ? toMongoDate(techProject.CompletionDate) : undefined
});
} else {
if (!hasAccessToDojo(inventory)) { if (!hasAccessToDojo(inventory)) {
res.status(400).send("-1").end(); res.status(400).send("-1").end();
return; return;
} }
const guild = await getGuildForRequestEx(req, inventory);
const guildMember = (await GuildMember.findOne( const guildMember = (await GuildMember.findOne(
{ accountId, guildId: guild._id }, { accountId, guildId: guild._id },
"RegularCreditsContributed MiscItemsContributed" "RegularCreditsContributed MiscItemsContributed"
))!; ))!;
const contributions = data; const techProject = guild.TechProjects!.find(x => x.ItemType == data.RecipeType)!;
const techProject = guild.TechProjects!.find(x => x.ItemType == contributions.RecipeType)!;
if (contributions.VaultCredits) { if (data.VaultCredits) {
if (contributions.VaultCredits > techProject.ReqCredits) { if (data.VaultCredits > techProject.ReqCredits) {
contributions.VaultCredits = techProject.ReqCredits; data.VaultCredits = techProject.ReqCredits;
} }
techProject.ReqCredits -= contributions.VaultCredits; techProject.ReqCredits -= data.VaultCredits;
guild.VaultRegularCredits! -= contributions.VaultCredits; guild.VaultRegularCredits! -= data.VaultCredits;
} }
if (contributions.RegularCredits > techProject.ReqCredits) { if (data.RegularCredits > techProject.ReqCredits) {
contributions.RegularCredits = techProject.ReqCredits; data.RegularCredits = techProject.ReqCredits;
} }
techProject.ReqCredits -= contributions.RegularCredits; techProject.ReqCredits -= data.RegularCredits;
guildMember.RegularCreditsContributed ??= 0; guildMember.RegularCreditsContributed ??= 0;
guildMember.RegularCreditsContributed += contributions.RegularCredits; guildMember.RegularCreditsContributed += data.RegularCredits;
if (contributions.VaultMiscItems.length) { if (data.VaultMiscItems.length) {
for (const miscItem of contributions.VaultMiscItems) { for (const miscItem of data.VaultMiscItems) {
const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType); const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType);
if (reqItem) { if (reqItem) {
if (miscItem.ItemCount > reqItem.ItemCount) { if (miscItem.ItemCount > reqItem.ItemCount) {
@ -131,7 +212,7 @@ export const guildTechController: RequestHandler = async (req, res) => {
} }
const miscItemChanges = []; const miscItemChanges = [];
for (const miscItem of contributions.MiscItems) { for (const miscItem of data.MiscItems) {
const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType); const reqItem = techProject.ReqItems.find(x => x.ItemType == miscItem.ItemType);
if (reqItem) { if (reqItem) {
if (miscItem.ItemCount > reqItem.ItemCount) { if (miscItem.ItemCount > reqItem.ItemCount) {
@ -143,37 +224,33 @@ export const guildTechController: RequestHandler = async (req, res) => {
ItemCount: miscItem.ItemCount * -1 ItemCount: miscItem.ItemCount * -1
}); });
guildMember.MiscItemsContributed ??= []; addGuildMemberMiscItemContribution(guildMember, miscItem);
guildMember.MiscItemsContributed.push(miscItem);
} }
} }
addMiscItems(inventory, miscItemChanges); addMiscItems(inventory, miscItemChanges);
const inventoryChanges: IInventoryChanges = updateCurrency(inventory, contributions.RegularCredits, false); const inventoryChanges: IInventoryChanges = updateCurrency(inventory, data.RegularCredits, false);
inventoryChanges.MiscItems = miscItemChanges; inventoryChanges.MiscItems = miscItemChanges;
if (techProject.ReqCredits == 0 && !techProject.ReqItems.find(x => x.ItemCount > 0)) { // Check if research is fully funded now.
// This research is now fully funded. await processGuildTechProjectContributionsUpdate(guild, techProject);
const recipe = ExportDojoRecipes.research[data.RecipeType];
processFundedProject(guild, techProject, recipe);
if (data.RecipeType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/") {
guild.ActiveDojoColorResearch = "";
await removePigmentsFromGuildMembers(guild._id);
}
}
await guild.save(); await Promise.all([guild.save(), inventory.save(), guildMember.save()]);
await inventory.save();
await guildMember.save();
res.json({ res.json({
InventoryChanges: inventoryChanges, InventoryChanges: inventoryChanges,
Vault: getGuildVault(guild) Vault: getGuildVault(guild)
}); });
}
} else if (data.Action.split(",")[0] == "Buy") { } else if (data.Action.split(",")[0] == "Buy") {
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) { const purchase = data as IGuildTechBuyRequest;
if (purchase.Mode == "Guild") {
const guild = await getGuildForRequestEx(req, inventory);
if (
!hasAccessToDojo(inventory) ||
!(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))
) {
res.status(400).send("-1").end(); res.status(400).send("-1").end();
return; return;
} }
const purchase = data as IGuildTechBuyRequest;
const quantity = parseInt(data.Action.split(",")[1]); const quantity = parseInt(data.Action.split(",")[1]);
const recipeChanges = [ const recipeChanges = [
{ {
@ -195,7 +272,15 @@ export const guildTechController: RequestHandler = async (req, res) => {
Recipes: recipeChanges Recipes: recipeChanges
} }
}); });
} else {
const inventoryChanges = claimSalvagedComponent(inventory, purchase.CategoryItemId!);
await inventory.save();
res.json({
inventoryChanges: inventoryChanges
});
}
} else if (data.Action == "Fabricate") { } else if (data.Action == "Fabricate") {
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) { if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) {
res.status(400).send("-1").end(); res.status(400).send("-1").end();
return; return;
@ -212,6 +297,7 @@ export const guildTechController: RequestHandler = async (req, res) => {
// Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`. // Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`.
res.json({ inventoryChanges: inventoryChanges }); res.json({ inventoryChanges: inventoryChanges });
} else if (data.Action == "Pause") { } else if (data.Action == "Pause") {
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) { if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
res.status(400).send("-1").end(); res.status(400).send("-1").end();
return; return;
@ -223,6 +309,7 @@ export const guildTechController: RequestHandler = async (req, res) => {
await removePigmentsFromGuildMembers(guild._id); await removePigmentsFromGuildMembers(guild._id);
res.end(); res.end();
} else if (data.Action == "Unpause") { } else if (data.Action == "Unpause") {
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) { if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
res.status(400).send("-1").end(); res.status(400).send("-1").end();
return; return;
@ -232,47 +319,67 @@ export const guildTechController: RequestHandler = async (req, res) => {
guild.ActiveDojoColorResearch = data.RecipeType; guild.ActiveDojoColorResearch = data.RecipeType;
await guild.save(); await guild.save();
res.end(); res.end();
} else { } else if (data.Action == "Cancel" && data.CategoryItemId) {
logger.debug(`data provided to ${req.path}: ${String(req.body)}`); const personalTechProjectIndex = inventory.PersonalTechProjects.findIndex(x =>
throw new Error(`unknown guildTech action: ${data.Action}`); x.CategoryItemId?.equals(data.CategoryItemId)
} );
}; const personalTechProject = inventory.PersonalTechProjects[personalTechProjectIndex];
inventory.PersonalTechProjects.splice(personalTechProjectIndex, 1);
const processFundedProject = ( const meta = ExportDojoRecipes.research[personalTechProject.ItemType];
guild: TGuildDatabaseDocument, const contributedCredits = meta.price - personalTechProject.ReqCredits;
techProject: ITechProjectDatabase, const inventoryChanges = updateCurrency(inventory, contributedCredits * -1, false);
recipe: IDojoResearch inventoryChanges.MiscItems = [];
): void => { for (const ingredient of meta.ingredients) {
techProject.State = 1; const reqItem = personalTechProject.ReqItems.find(x => x.ItemType == ingredient.ItemType);
techProject.CompletionDate = new Date(Date.now() + (config.noDojoResearchTime ? 0 : recipe.time) * 1000); if (reqItem) {
if (recipe.guildXpValue) { const contributedItems = ingredient.ItemCount - reqItem.ItemCount;
guild.XP += recipe.guildXpValue; inventoryChanges.MiscItems.push({
} ItemType: ingredient.ItemType,
setTechLogState(guild, techProject.ItemType, config.noDojoResearchTime ? 4 : 3, techProject.CompletionDate); ItemCount: contributedItems
};
const setTechLogState = (
guild: TGuildDatabaseDocument,
type: string,
state: number,
dateTime: Date | undefined = undefined
): boolean => {
guild.TechChanges ??= [];
const entry = guild.TechChanges.find(x => x.details == type);
if (entry) {
if (entry.entryType == state) {
return false;
}
entry.dateTime = dateTime;
entry.entryType = state;
} else {
guild.TechChanges.push({
dateTime: dateTime,
entryType: state,
details: type
}); });
} }
return true; }
addMiscItems(inventory, inventoryChanges.MiscItems);
await inventory.save();
res.json({
action: "Cancel",
isPersonal: true,
inventoryChanges: inventoryChanges,
personalTech: {
ItemId: toOid(personalTechProject._id)
}
});
} else if (data.Action == "Rush" && data.CategoryItemId) {
const inventoryChanges: IInventoryChanges = {
...updateCurrency(inventory, 20, true),
...claimSalvagedComponent(inventory, data.CategoryItemId)
};
await inventory.save();
res.json({
inventoryChanges: inventoryChanges
});
} else if (data.Action == "InstantFinish") {
if (data.TechProductCategory != "CrewShipWeapons" && data.TechProductCategory != "CrewShipWeaponSkins") {
throw new Error(`unexpected TechProductCategory: ${data.TechProductCategory}`);
}
const inventoryChanges = finishComponentRepair(inventory, data.TechProductCategory, data.CategoryItemId!);
inventoryChanges.MiscItems = [
{
ItemType: "/Lotus/Types/Items/MiscItems/InstantSalvageRepairItem",
ItemCount: -1
}
];
addMiscItems(inventory, inventoryChanges.MiscItems);
await inventory.save();
res.json({
inventoryChanges: inventoryChanges
});
} else {
logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
throw new Error(`unhandled guildTech request`);
}
}; };
type TGuildTechRequest = type TGuildTechRequest =
@ -281,23 +388,77 @@ type TGuildTechRequest =
| IGuildTechContributeRequest; | IGuildTechContributeRequest;
interface IGuildTechBasicRequest { interface IGuildTechBasicRequest {
Action: "Start" | "Fabricate" | "Pause" | "Unpause"; Action: "Start" | "Fabricate" | "Pause" | "Unpause" | "Cancel" | "Rush" | "InstantFinish";
Mode: "Guild"; Mode: "Guild" | "Personal";
RecipeType: string; RecipeType: string;
TechProductCategory?: string;
CategoryItemId?: string;
} }
interface IGuildTechBuyRequest { interface IGuildTechBuyRequest extends Omit<IGuildTechBasicRequest, "Action"> {
Action: string; Action: string;
Mode: "Guild";
RecipeType: string;
} }
interface IGuildTechContributeRequest { interface IGuildTechContributeRequest {
Action: "Contribute"; Action: "Contribute";
ResearchId: ""; ResearchId: string;
RecipeType: string; RecipeType: string;
RegularCredits: number; RegularCredits: number;
MiscItems: IMiscItem[]; MiscItems: IMiscItem[];
VaultCredits: number; VaultCredits: number;
VaultMiscItems: IMiscItem[]; VaultMiscItems: IMiscItem[];
} }
const getSalvageCategory = (
category: "CrewShipWeapons" | "CrewShipWeaponSkins"
): "CrewShipSalvagedWeapons" | "CrewShipSalvagedWeaponSkins" => {
return category == "CrewShipWeapons" ? "CrewShipSalvagedWeapons" : "CrewShipSalvagedWeaponSkins";
};
const claimSalvagedComponent = (inventory: TInventoryDatabaseDocument, itemId: string): IInventoryChanges => {
// delete personal tech project
const personalTechProjectIndex = inventory.PersonalTechProjects.findIndex(x => x.CategoryItemId?.equals(itemId));
const personalTechProject = inventory.PersonalTechProjects[personalTechProjectIndex];
inventory.PersonalTechProjects.splice(personalTechProjectIndex, 1);
const category = personalTechProject.ProductCategory! as "CrewShipWeapons" | "CrewShipWeaponSkins";
return finishComponentRepair(inventory, category, itemId);
};
const finishComponentRepair = (
inventory: TInventoryDatabaseDocument,
category: "CrewShipWeapons" | "CrewShipWeaponSkins",
itemId: string
): IInventoryChanges => {
const salvageCategory = getSalvageCategory(category);
// find salved part & delete it
const salvageIndex = inventory[salvageCategory].findIndex(x => x._id.equals(itemId));
const salvageItem = inventory[salvageCategory][salvageIndex];
inventory[salvageCategory].splice(salvageIndex, 1);
// add final item
const inventoryChanges = {
...(category == "CrewShipWeaponSkins"
? addCrewShipWeaponSkin(inventory, salvageItem.ItemType, salvageItem.UpgradeFingerprint)
: addEquipment(
inventory,
category,
salvageItem.ItemType,
undefined,
{},
{
UpgradeFingerprint: salvageItem.UpgradeFingerprint
}
)),
...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, false)
};
inventoryChanges.RemovedIdItems = [
{
ItemId: { $oid: itemId }
}
];
return inventoryChanges;
};

View File

@ -34,8 +34,8 @@ export const inboxController: RequestHandler = async (req, res) => {
message.r = true; message.r = true;
await message.save(); await message.save();
const attachmentItems = message.att; const attachmentItems = message.attVisualOnly ? undefined : message.att;
const attachmentCountedItems = message.countedAtt; const attachmentCountedItems = message.attVisualOnly ? undefined : message.countedAtt;
if (!attachmentItems && !attachmentCountedItems && !message.gifts) { if (!attachmentItems && !attachmentCountedItems && !message.gifts) {
res.status(200).end(); res.status(200).end();
@ -67,7 +67,7 @@ export const inboxController: RequestHandler = async (req, res) => {
(await handleStoreItemAcquisition(gift.GiftType, inventory, giftQuantity)).InventoryChanges (await handleStoreItemAcquisition(gift.GiftType, inventory, giftQuantity)).InventoryChanges
); );
if (sender) { if (sender) {
await createMessage(sender._id.toString(), [ await createMessage(sender._id, [
{ {
sndr: recipientName, sndr: recipientName,
msg: "/Lotus/Language/Menu/GiftReceivedConfirmationBody", msg: "/Lotus/Language/Menu/GiftReceivedConfirmationBody",

View File

@ -6,20 +6,21 @@ import { IOid } from "@/src/types/commonTypes";
import { import {
IConsumedSuit, IConsumedSuit,
IHelminthFoodRecord, IHelminthFoodRecord,
IInfestedFoundryClient,
IInfestedFoundryDatabase,
IInventoryClient, IInventoryClient,
IMiscItem, IMiscItem,
InventorySlot, InventorySlot
ITypeCount
} from "@/src/types/inventoryTypes/inventoryTypes"; } from "@/src/types/inventoryTypes/inventoryTypes";
import { ExportMisc, ExportRecipes } from "warframe-public-export-plus"; import { ExportMisc } from "warframe-public-export-plus";
import { getRecipe } from "@/src/services/itemDataService"; import { getRecipe } from "@/src/services/itemDataService";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { toMongoDate } from "@/src/helpers/inventoryHelpers"; import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { colorToShard } from "@/src/helpers/shardHelper"; import { colorToShard } from "@/src/helpers/shardHelper";
import { config } from "@/src/services/configService"; import { config } from "@/src/services/configService";
import {
addInfestedFoundryXP,
applyCheatsToInfestedFoundry,
handleSubsumeCompletion
} from "@/src/services/infestedFoundryService";
export const infestedFoundryController: RequestHandler = async (req, res) => { export const infestedFoundryController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
@ -28,7 +29,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
// shard installation // shard installation
const request = getJSONfromString<IShardInstallRequest>(String(req.body)); const request = getJSONfromString<IShardInstallRequest>(String(req.body));
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const suit = inventory.Suits.find(suit => suit._id.toString() == request.SuitId.$oid)!; const suit = inventory.Suits.id(request.SuitId.$oid)!;
if (!suit.ArchonCrystalUpgrades || suit.ArchonCrystalUpgrades.length != 5) { if (!suit.ArchonCrystalUpgrades || suit.ArchonCrystalUpgrades.length != 5) {
suit.ArchonCrystalUpgrades = [{}, {}, {}, {}, {}]; suit.ArchonCrystalUpgrades = [{}, {}, {}, {}, {}];
} }
@ -56,7 +57,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
// shard removal // shard removal
const request = getJSONfromString<IShardUninstallRequest>(String(req.body)); const request = getJSONfromString<IShardUninstallRequest>(String(req.body));
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const suit = inventory.Suits.find(suit => suit._id.toString() == request.SuitId.$oid)!; const suit = inventory.Suits.id(request.SuitId.$oid)!;
const miscItemChanges: IMiscItem[] = []; const miscItemChanges: IMiscItem[] = [];
if (suit.ArchonCrystalUpgrades![request.Slot].Color) { if (suit.ArchonCrystalUpgrades![request.Slot].Color) {
@ -383,116 +384,11 @@ interface IHelminthFeedRequest {
}[]; }[];
} }
export const addInfestedFoundryXP = (infestedFoundry: IInfestedFoundryDatabase, delta: number): ITypeCount[] => {
const recipeChanges: ITypeCount[] = [];
infestedFoundry.XP ??= 0;
const prevXP = infestedFoundry.XP;
infestedFoundry.XP += delta;
if (prevXP < 2250_00 && infestedFoundry.XP >= 2250_00) {
infestedFoundry.Slots ??= 0;
infestedFoundry.Slots += 3;
}
if (prevXP < 5625_00 && infestedFoundry.XP >= 5625_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthShieldsBlueprint",
ItemCount: 1
});
}
if (prevXP < 10125_00 && infestedFoundry.XP >= 10125_00) {
recipeChanges.push({ ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthHackBlueprint", ItemCount: 1 });
}
if (prevXP < 15750_00 && infestedFoundry.XP >= 15750_00) {
infestedFoundry.Slots ??= 0;
infestedFoundry.Slots += 10;
}
if (prevXP < 22500_00 && infestedFoundry.XP >= 22500_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthAmmoEfficiencyBlueprint",
ItemCount: 1
});
}
if (prevXP < 30375_00 && infestedFoundry.XP >= 30375_00) {
recipeChanges.push({ ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthStunBlueprint", ItemCount: 1 });
}
if (prevXP < 39375_00 && infestedFoundry.XP >= 39375_00) {
infestedFoundry.Slots ??= 0;
infestedFoundry.Slots += 20;
}
if (prevXP < 60750_00 && infestedFoundry.XP >= 60750_00) {
recipeChanges.push({ ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthStatusBlueprint", ItemCount: 1 });
}
if (prevXP < 73125_00 && infestedFoundry.XP >= 73125_00) {
infestedFoundry.Slots = 1;
}
if (prevXP < 86625_00 && infestedFoundry.XP >= 86625_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthShieldArmorBlueprint",
ItemCount: 1
});
}
if (prevXP < 101250_00 && infestedFoundry.XP >= 101250_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthProcBlockBlueprint",
ItemCount: 1
});
}
if (prevXP < 117000_00 && infestedFoundry.XP >= 117000_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthEnergyShareBlueprint",
ItemCount: 1
});
}
if (prevXP < 133875_00 && infestedFoundry.XP >= 133875_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthMaxStatusBlueprint",
ItemCount: 1
});
}
if (prevXP < 151875_00 && infestedFoundry.XP >= 151875_00) {
recipeChanges.push({
ItemType: "/Lotus/Types/Recipes/AbilityOverrides/HelminthTreasureBlueprint",
ItemCount: 1
});
}
return recipeChanges;
};
interface IHelminthSubsumeRequest { interface IHelminthSubsumeRequest {
SuitId: IOid; SuitId: IOid;
Recipe: string; Recipe: string;
} }
export const handleSubsumeCompletion = (inventory: TInventoryDatabaseDocument): ITypeCount[] => {
const [recipeType] = Object.entries(ExportRecipes).find(
([_recipeType, recipe]) =>
recipe.secretIngredientAction == "SIA_WARFRAME_ABILITY" &&
recipe.secretIngredients![0].ItemType == inventory.InfestedFoundry!.LastConsumedSuit!.ItemType
)!;
inventory.InfestedFoundry!.LastConsumedSuit = undefined;
inventory.InfestedFoundry!.AbilityOverrideUnlockCooldown = undefined;
const recipeChanges: ITypeCount[] = [
{
ItemType: recipeType,
ItemCount: 1
}
];
addRecipes(inventory, recipeChanges);
return recipeChanges;
};
export const applyCheatsToInfestedFoundry = (infestedFoundry: IInfestedFoundryClient): void => {
if (config.infiniteHelminthMaterials) {
infestedFoundry.Resources = [
{ ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthCalx", Count: 1000 },
{ ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthBiotics", Count: 1000 },
{ ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthSynthetics", Count: 1000 },
{ ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthPheromones", Count: 1000 },
{ ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthBile", Count: 1000 },
{ ItemType: "/Lotus/Types/Items/InfestedFoundry/HelminthOxides", Count: 1000 }
];
}
};
interface IHelminthOfferingsUpdate { interface IHelminthOfferingsUpdate {
OfferingsIndex: number; OfferingsIndex: number;
SuitTypes: string[]; SuitTypes: string[];

View File

@ -13,9 +13,10 @@ import {
ExportResources, ExportResources,
ExportVirtuals ExportVirtuals
} from "warframe-public-export-plus"; } from "warframe-public-export-plus";
import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "./infestedFoundryController"; import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "@/src/services/infestedFoundryService";
import { addMiscItems, allDailyAffiliationKeys, createLibraryDailyTask } from "@/src/services/inventoryService"; import { addMiscItems, allDailyAffiliationKeys, createLibraryDailyTask } from "@/src/services/inventoryService";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { catBreadHash } from "@/src/helpers/stringHelpers";
export const inventoryController: RequestHandler = async (request, response) => { export const inventoryController: RequestHandler = async (request, response) => {
const accountId = await getAccountIdForRequest(request); const accountId = await getAccountIdForRequest(request);
@ -34,6 +35,7 @@ export const inventoryController: RequestHandler = async (request, response) =>
} }
inventory.DailyFocus = 250000 + inventory.PlayerLevel * 5000; inventory.DailyFocus = 250000 + inventory.PlayerLevel * 5000;
inventory.GiftsRemaining = Math.max(8, inventory.PlayerLevel); inventory.GiftsRemaining = Math.max(8, inventory.PlayerLevel);
inventory.TradesRemaining = inventory.PlayerLevel;
inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask(); inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask();
@ -51,9 +53,11 @@ export const inventoryController: RequestHandler = async (request, response) =>
if (numArgonCrystals == 0) { if (numArgonCrystals == 0) {
break; break;
} }
const numStableArgonCrystals = const numStableArgonCrystals = Math.min(
numArgonCrystals,
inventory.FoundToday?.find(x => x.ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal") inventory.FoundToday?.find(x => x.ItemType == "/Lotus/Types/Items/MiscItems/ArgonCrystal")
?.ItemCount ?? 0; ?.ItemCount ?? 0
);
const numDecayingArgonCrystals = numArgonCrystals - numStableArgonCrystals; const numDecayingArgonCrystals = numArgonCrystals - numStableArgonCrystals;
const numDecayingArgonCrystalsToRemove = Math.ceil(numDecayingArgonCrystals / 2); const numDecayingArgonCrystalsToRemove = Math.ceil(numDecayingArgonCrystals / 2);
logger.debug(`ticking argon crystals for day ${i + 1} of ${daysPassed}`, { logger.debug(`ticking argon crystals for day ${i + 1} of ${daysPassed}`, {
@ -145,7 +149,7 @@ export const getInventoryResponse = async (
inventoryResponse.ShipDecorations = []; inventoryResponse.ShipDecorations = [];
for (const [uniqueName, item] of Object.entries(ExportResources)) { for (const [uniqueName, item] of Object.entries(ExportResources)) {
if (item.productCategory == "ShipDecorations") { if (item.productCategory == "ShipDecorations") {
inventoryResponse.ShipDecorations.push({ ItemType: uniqueName, ItemCount: 1 }); inventoryResponse.ShipDecorations.push({ ItemType: uniqueName, ItemCount: 999_999 });
} }
} }
} }
@ -198,7 +202,8 @@ export const getInventoryResponse = async (
if (config.universalPolarityEverywhere) { if (config.universalPolarityEverywhere) {
const Polarity: IPolarity[] = []; const Polarity: IPolarity[] = [];
for (let i = 0; i != 12; ++i) { // 12 is needed for necramechs. 15 is needed for plexus/crewshipharness.
for (let i = 0; i != 15; ++i) {
Polarity.push({ Polarity.push({
Slot: i, Slot: i,
Value: ArtifactPolarity.Any Value: ArtifactPolarity.Any
@ -253,6 +258,10 @@ export const getInventoryResponse = async (
} }
} }
if (config.noDailyFocusLimit) {
inventoryResponse.DailyFocus = Math.max(999_999, 250000 + inventoryResponse.PlayerLevel * 5000);
}
if (inventoryResponse.InfestedFoundry) { if (inventoryResponse.InfestedFoundry) {
applyCheatsToInfestedFoundry(inventoryResponse.InfestedFoundry); applyCheatsToInfestedFoundry(inventoryResponse.InfestedFoundry);
} }
@ -266,7 +275,7 @@ export const getInventoryResponse = async (
return inventoryResponse; return inventoryResponse;
}; };
export const addString = (arr: string[], str: string): void => { const addString = (arr: string[], str: string): void => {
if (!arr.find(x => x == str)) { if (!arr.find(x => x == str)) {
arr.push(str); arr.push(str);
} }
@ -296,13 +305,3 @@ const resourceGetParent = (resourceName: string): string | undefined => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
return ExportVirtuals[resourceName]?.parentName; return ExportVirtuals[resourceName]?.parentName;
}; };
// This is FNV1a-32 except operating under modulus 2^31 because JavaScript is stinky and likes producing negative integers out of nowhere.
export const catBreadHash = (name: string): number => {
let hash = 2166136261;
for (let i = 0; i != name.length; ++i) {
hash = (hash ^ name.charCodeAt(i)) & 0x7fffffff;
hash = (hash * 16777619) & 0x7fffffff;
}
return hash;
};

View File

@ -54,8 +54,12 @@ export const loginController: RequestHandler = async (request, response) => {
} }
} }
//email not found or incorrect password if (!account) {
if (!account || !isCorrectPassword(loginRequest.password, account.password)) { response.status(400).json({ error: "unknown user" });
return;
}
if (!isCorrectPassword(loginRequest.password, account.password)) {
response.status(400).json({ error: "incorrect login data" }); response.status(400).json({ error: "incorrect login data" });
return; return;
} }

View File

@ -47,16 +47,21 @@ import { logger } from "@/src/utils/logger";
- [ ] FpsSamples - [ ] FpsSamples
*/ */
//move credit calc in here, return MissionRewards: [] if no reward info //move credit calc in here, return MissionRewards: [] if no reward info
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const missionInventoryUpdateController: RequestHandler = async (req, res): Promise<void> => { export const missionInventoryUpdateController: RequestHandler = async (req, res): Promise<void> => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const missionReport = getJSONfromString<IMissionInventoryUpdateRequest>((req.body as string).toString()); const missionReport = getJSONfromString<IMissionInventoryUpdateRequest>((req.body as string).toString());
logger.debug("mission report:", missionReport); logger.debug("mission report:", missionReport);
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const firstCompletion = missionReport.SortieId
? inventory.CompletedSorties.indexOf(missionReport.SortieId) == -1
: false;
const inventoryUpdates = await addMissionInventoryUpdates(inventory, missionReport); const inventoryUpdates = await addMissionInventoryUpdates(inventory, missionReport);
if (missionReport.MissionStatus !== "GS_SUCCESS") { if (
missionReport.MissionStatus !== "GS_SUCCESS" &&
!(missionReport.RewardInfo?.jobId || missionReport.RewardInfo?.challengeMissionId)
) {
await inventory.save(); await inventory.save();
const inventoryResponse = await getInventoryResponse(inventory, true); const inventoryResponse = await getInventoryResponse(inventory, true);
res.json({ res.json({
@ -66,7 +71,8 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
return; return;
} }
const { MissionRewards, inventoryChanges, credits } = await addMissionRewards(inventory, missionReport); const { MissionRewards, inventoryChanges, credits, AffiliationMods, SyndicateXPItemReward } =
await addMissionRewards(inventory, missionReport, firstCompletion);
await inventory.save(); await inventory.save();
const inventoryResponse = await getInventoryResponse(inventory, true); const inventoryResponse = await getInventoryResponse(inventory, true);
@ -78,7 +84,9 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
MissionRewards, MissionRewards,
...credits, ...credits,
...inventoryUpdates, ...inventoryUpdates,
FusionPoints: inventoryChanges?.FusionPoints //FusionPoints: inventoryChanges?.FusionPoints, // This in combination with InventoryJson or InventoryChanges seems to just double the number of endo shown, so unsure when this is needed.
SyndicateXPItemReward,
AffiliationMods
}); });
}; };

View File

@ -17,12 +17,13 @@ import { getDefaultUpgrades } from "@/src/services/itemDataService";
import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper"; import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper";
import { IEquipmentDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { IEquipmentDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { getRandomInt } from "@/src/services/rngService"; import { getRandomInt } from "@/src/services/rngService";
import { ExportSentinels } from "warframe-public-export-plus"; import { ExportSentinels, IDefaultUpgrade } from "warframe-public-export-plus";
import { Status } from "@/src/types/inventoryTypes/inventoryTypes"; import { Status } from "@/src/types/inventoryTypes/inventoryTypes";
interface IModularCraftRequest { interface IModularCraftRequest {
WeaponType: string; WeaponType: string;
Parts: string[]; Parts: string[];
isWebUi?: boolean;
} }
export const modularWeaponCraftingController: RequestHandler = async (req, res) => { export const modularWeaponCraftingController: RequestHandler = async (req, res) => {
@ -34,10 +35,8 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res)
const category = modularWeaponTypes[data.WeaponType]; const category = modularWeaponTypes[data.WeaponType];
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const defaultUpgrades = getDefaultUpgrades(data.Parts); let defaultUpgrades: IDefaultUpgrade[] | undefined;
const defaultOverwrites: Partial<IEquipmentDatabase> = { const defaultOverwrites: Partial<IEquipmentDatabase> = {};
Configs: applyDefaultUpgrades(inventory, defaultUpgrades)
};
const inventoryChanges: IInventoryChanges = {}; const inventoryChanges: IInventoryChanges = {};
if (category == "KubrowPets") { if (category == "KubrowPets") {
const traits = { const traits = {
@ -129,25 +128,37 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res)
// Only save mutagen & antigen in the ModularParts. // Only save mutagen & antigen in the ModularParts.
defaultOverwrites.ModularParts = [data.Parts[1], data.Parts[2]]; defaultOverwrites.ModularParts = [data.Parts[1], data.Parts[2]];
for (const specialItem of ExportSentinels[data.WeaponType].exalted!) { const meta = ExportSentinels[data.WeaponType];
for (const specialItem of meta.exalted!) {
addSpecialItem(inventory, specialItem, inventoryChanges); addSpecialItem(inventory, specialItem, inventoryChanges);
} }
defaultUpgrades = meta.defaultUpgrades;
} else {
defaultUpgrades = getDefaultUpgrades(data.Parts);
} }
defaultOverwrites.Configs = applyDefaultUpgrades(inventory, defaultUpgrades);
addEquipment(inventory, category, data.WeaponType, data.Parts, inventoryChanges, defaultOverwrites); addEquipment(inventory, category, data.WeaponType, data.Parts, inventoryChanges, defaultOverwrites);
combineInventoryChanges(inventoryChanges, occupySlot(inventory, productCategoryToInventoryBin(category)!, false)); combineInventoryChanges(
inventoryChanges,
occupySlot(inventory, productCategoryToInventoryBin(category)!, !!data.isWebUi)
);
if (defaultUpgrades) { if (defaultUpgrades) {
inventoryChanges.RawUpgrades = defaultUpgrades.map(x => ({ ItemType: x.ItemType, ItemCount: 1 })); inventoryChanges.RawUpgrades = defaultUpgrades.map(x => ({ ItemType: x.ItemType, ItemCount: 1 }));
} }
// Remove credits & parts // Remove credits & parts
const miscItemChanges = []; const miscItemChanges = [];
let currencyChanges = {};
if (!data.isWebUi) {
for (const part of data.Parts) { for (const part of data.Parts) {
miscItemChanges.push({ miscItemChanges.push({
ItemType: part, ItemType: part,
ItemCount: -1 ItemCount: -1
}); });
} }
const currencyChanges = updateCurrency( currencyChanges = updateCurrency(
inventory, inventory,
category == "Hoverboards" || category == "Hoverboards" ||
category == "MoaPets" || category == "MoaPets" ||
@ -159,8 +170,9 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res)
false false
); );
addMiscItems(inventory, miscItemChanges); addMiscItems(inventory, miscItemChanges);
await inventory.save(); }
await inventory.save();
// Tell client what we did // Tell client what we did
res.json({ res.json({
InventoryChanges: { InventoryChanges: {

View File

@ -22,7 +22,6 @@ export const modularWeaponSaleController: RequestHandler = async (req, res) => {
const partTypeToParts: Record<string, string[]> = {}; const partTypeToParts: Record<string, string[]> = {};
for (const [uniqueName, data] of Object.entries(ExportWeapons)) { for (const [uniqueName, data] of Object.entries(ExportWeapons)) {
if (data.partType && data.premiumPrice) { if (data.partType && data.premiumPrice) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
partTypeToParts[data.partType] ??= []; partTypeToParts[data.partType] ??= [];
partTypeToParts[data.partType].push(uniqueName); partTypeToParts[data.partType].push(uniqueName);
} }

View File

@ -12,15 +12,17 @@ export const nameWeaponController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const body = getJSONfromString<INameWeaponRequest>(String(req.body)); const body = getJSONfromString<INameWeaponRequest>(String(req.body));
const item = inventory[req.query.Category as string as TEquipmentKey].find( const item = inventory[req.query.Category as string as TEquipmentKey].id(req.query.ItemId as string)!;
item => item._id.toString() == (req.query.ItemId as string)
)!;
if (body.ItemName != "") { if (body.ItemName != "") {
item.ItemName = body.ItemName; item.ItemName = body.ItemName;
} else { } else {
item.ItemName = undefined; item.ItemName = undefined;
} }
const currencyChanges = updateCurrency(inventory, "webui" in req.query ? 0 : 15, true); const currencyChanges = updateCurrency(
inventory,
req.query.Category == "Horses" || "webui" in req.query ? 0 : 15,
true
);
await inventory.save(); await inventory.save();
res.json({ res.json({
InventoryChanges: currencyChanges InventoryChanges: currencyChanges

View File

@ -1,10 +1,25 @@
import { getInfNodes, getNemesisPasscode } from "@/src/helpers/nemesisHelpers"; import {
consumeModCharge,
encodeNemesisGuess,
getInfNodes,
getNemesisPasscode,
IKnifeResponse
} from "@/src/helpers/nemesisHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
import { freeUpSlot, getInventory } from "@/src/services/inventoryService"; import { freeUpSlot, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { SRng } from "@/src/services/rngService"; import { SRng } from "@/src/services/rngService";
import { IMongoDate, IOid } from "@/src/types/commonTypes"; import { IMongoDate, IOid } from "@/src/types/commonTypes";
import { IInnateDamageFingerprint, InventorySlot, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import {
IInnateDamageFingerprint,
InventorySlot,
IUpgradeClient,
IWeaponSkinClient,
LoadoutIndex,
TEquipmentKey
} from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
@ -18,7 +33,7 @@ export const nemesisController: RequestHandler = async (req, res) => {
const destFingerprint = JSON.parse(destWeapon.UpgradeFingerprint!) as IInnateDamageFingerprint; const destFingerprint = JSON.parse(destWeapon.UpgradeFingerprint!) as IInnateDamageFingerprint;
const sourceFingerprint = JSON.parse(sourceWeapon.UpgradeFingerprint!) as IInnateDamageFingerprint; const sourceFingerprint = JSON.parse(sourceWeapon.UpgradeFingerprint!) as IInnateDamageFingerprint;
// Upgrade destination damage type if desireed // Update destination damage type if desired
if (body.UseSourceDmgType) { if (body.UseSourceDmgType) {
destFingerprint.buffs[0].Tag = sourceFingerprint.buffs[0].Tag; destFingerprint.buffs[0].Tag = sourceFingerprint.buffs[0].Tag;
} }
@ -27,7 +42,7 @@ export const nemesisController: RequestHandler = async (req, res) => {
const destDamage = 0.25 + (destFingerprint.buffs[0].Value / 0x3fffffff) * (0.6 - 0.25); const destDamage = 0.25 + (destFingerprint.buffs[0].Value / 0x3fffffff) * (0.6 - 0.25);
const sourceDamage = 0.25 + (sourceFingerprint.buffs[0].Value / 0x3fffffff) * (0.6 - 0.25); const sourceDamage = 0.25 + (sourceFingerprint.buffs[0].Value / 0x3fffffff) * (0.6 - 0.25);
let newDamage = Math.max(destDamage, sourceDamage) * 1.1; let newDamage = Math.max(destDamage, sourceDamage) * 1.1;
if (newDamage >= 0.58) { if (newDamage >= 0.5794998) {
newDamage = 0.6; newDamage = 0.6;
} }
destFingerprint.buffs[0].Value = Math.trunc(((newDamage - 0.25) / (0.6 - 0.25)) * 0x3fffffff); destFingerprint.buffs[0].Value = Math.trunc(((newDamage - 0.25) / (0.6 - 0.25)) * 0x3fffffff);
@ -42,13 +57,14 @@ export const nemesisController: RequestHandler = async (req, res) => {
await inventory.save(); await inventory.save();
res.json({ res.json({
InventoryChanges: { InventoryChanges: {
[body.Category]: [destWeapon.toJSON()] [body.Category]: [destWeapon.toJSON()],
RemovedIdItems: [{ ItemId: body.SourceWeapon }]
} }
}); });
} else if ((req.query.mode as string) == "p") { } else if ((req.query.mode as string) == "p") {
const inventory = await getInventory(accountId, "Nemesis"); const inventory = await getInventory(accountId, "Nemesis");
const body = getJSONfromString<INemesisPrespawnCheckRequest>(String(req.body)); const body = getJSONfromString<INemesisPrespawnCheckRequest>(String(req.body));
const passcode = getNemesisPasscode(inventory.Nemesis!.fp, inventory.Nemesis!.Faction); const passcode = getNemesisPasscode(inventory.Nemesis!);
let guessResult = 0; let guessResult = 0;
if (inventory.Nemesis!.Faction == "FC_INFESTATION") { if (inventory.Nemesis!.Faction == "FC_INFESTATION") {
for (let i = 0; i != 3; ++i) { for (let i = 0; i != 3; ++i) {
@ -65,6 +81,88 @@ export const nemesisController: RequestHandler = async (req, res) => {
} }
} }
res.json({ GuessResult: guessResult }); res.json({ GuessResult: guessResult });
} else if (req.query.mode == "r") {
const inventory = await getInventory(
accountId,
"Nemesis LoadOutPresets CurrentLoadOutIds DataKnives Upgrades RawUpgrades"
);
const body = getJSONfromString<INemesisRequiemRequest>(String(req.body));
if (inventory.Nemesis!.Faction == "FC_INFESTATION") {
const guess: number[] = [body.guess & 0xf, (body.guess >> 4) & 0xf, (body.guess >> 8) & 0xf];
const passcode = getNemesisPasscode(inventory.Nemesis!)[0];
// Add to GuessHistory
const result1 = passcode == guess[0] ? 0 : 1;
const result2 = passcode == guess[1] ? 0 : 1;
const result3 = passcode == guess[2] ? 0 : 1;
inventory.Nemesis!.GuessHistory.push(
encodeNemesisGuess(guess[0], result1, guess[1], result2, guess[2], result3)
);
// Increase antivirus
let antivirusGain = 5;
const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!;
const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid);
const dataknifeConfigIndex = dataknifeLoadout?.s?.mod ?? 0;
const dataknifeUpgrades = inventory.DataKnives[0].Configs[dataknifeConfigIndex].Upgrades!;
const response: IKnifeResponse = {};
for (const upgrade of body.knife!.AttachedUpgrades) {
switch (upgrade.ItemType) {
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndSpeedOnUseMod":
antivirusGain += 10;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndWeaponDamageOnUseMod":
antivirusGain += 10;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusLargeOnSingleUseMod": // Instant Secure
antivirusGain += 15;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusOnUseMod": // Immuno Shield
antivirusGain += 15;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusSmallOnSingleUseMod":
antivirusGain += 10;
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
break;
}
}
inventory.Nemesis!.HenchmenKilled += antivirusGain;
if (inventory.Nemesis!.HenchmenKilled >= 100) {
inventory.Nemesis!.HenchmenKilled = 100;
inventory.Nemesis!.InfNodes = [
{
Node: "CrewBattleNode559",
Influence: 1
}
];
inventory.Nemesis!.Weakened = true;
} else {
inventory.Nemesis!.InfNodes = getInfNodes("FC_INFESTATION", 0);
}
await inventory.save();
res.json(response);
} else {
const passcode = getNemesisPasscode(inventory.Nemesis!);
if (passcode[body.position] != body.guess) {
res.end();
} else {
inventory.Nemesis!.Rank += 1;
inventory.Nemesis!.InfNodes = getInfNodes(inventory.Nemesis!.Faction, inventory.Nemesis!.Rank);
await inventory.save();
res.json({ RankIncrease: 1 });
}
}
} else if ((req.query.mode as string) == "rs") {
// report spawn; POST but no application data in body
const inventory = await getInventory(accountId, "Nemesis");
inventory.Nemesis!.LastEnc = inventory.Nemesis!.MissionCount;
await inventory.save();
res.json({ LastEnc: inventory.Nemesis!.LastEnc });
} else if ((req.query.mode as string) == "s") { } else if ((req.query.mode as string) == "s") {
const inventory = await getInventory(accountId, "Nemesis"); const inventory = await getInventory(accountId, "Nemesis");
const body = getJSONfromString<INemesisStartRequest>(String(req.body)); const body = getJSONfromString<INemesisStartRequest>(String(req.body));
@ -172,6 +270,20 @@ interface INemesisPrespawnCheckRequest {
potency?: number[]; potency?: number[];
} }
interface INemesisRequiemRequest {
guess: number; // grn/crp: 4 bits | coda: 3x 4 bits
position: number; // grn/crp: 0-2 | coda: 0
// knife field provided for coda only
knife?: {
Item: IEquipmentClient;
Skins: IWeaponSkinClient[];
ModSlot: number;
CustSlot: number;
AttachedUpgrades: IUpgradeClient[];
HiddenWhenHolstered: boolean;
};
}
const kuvaLichVersionSixWeapons = [ const kuvaLichVersionSixWeapons = [
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Drakgoon/KuvaDrakgoon", "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Drakgoon/KuvaDrakgoon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Karak/KuvaKarak", "/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Karak/KuvaKarak",

View File

@ -1,10 +1,19 @@
import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService"; import {
getDojoClient,
getGuildForRequestEx,
getVaultMiscItemCount,
hasAccessToDojo,
hasGuildPermission,
processDojoBuildMaterialsGathered,
scaleRequiredCount
} from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes"; import { GuildPermission } from "@/src/types/guildTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { ExportDojoRecipes } from "warframe-public-export-plus"; import { ExportDojoRecipes, ExportResources } from "warframe-public-export-plus";
import { config } from "@/src/services/configService";
export const placeDecoInComponentController: RequestHandler = async (req, res) => { export const placeDecoInComponentController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
@ -24,6 +33,11 @@ export const placeDecoInComponentController: RequestHandler = async (req, res) =
} }
component.Decos ??= []; component.Decos ??= [];
if (request.MoveId) {
const deco = component.Decos.find(x => x._id.equals(request.MoveId))!;
deco.Pos = request.Pos;
deco.Rot = request.Rot;
} else {
const deco = const deco =
component.Decos[ component.Decos[
component.Decos.push({ component.Decos.push({
@ -31,17 +45,61 @@ export const placeDecoInComponentController: RequestHandler = async (req, res) =
Type: request.Type, Type: request.Type,
Pos: request.Pos, Pos: request.Pos,
Rot: request.Rot, Rot: request.Rot,
Name: request.Name Name: request.Name,
Sockets: request.Sockets
}) - 1 }) - 1
]; ];
const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == request.Type); const meta = Object.values(ExportDojoRecipes.decos).find(x => x.resultType == request.Type);
if (meta) { if (meta) {
if (meta.capacityCost) { if (meta.capacityCost) {
component.DecoCapacity -= meta.capacityCost; component.DecoCapacity -= meta.capacityCost;
} }
if (meta.price == 0 && meta.ingredients.length == 0) { } else {
const itemType = Object.entries(ExportResources).find(arr => arr[1].deco == deco.Type)![0];
if (deco.Sockets !== undefined) {
guild.VaultFusionTreasures!.find(x => x.ItemType == itemType && x.Sockets == deco.Sockets)!.ItemCount -=
1;
} else {
guild.VaultShipDecorations!.find(x => x.ItemType == itemType)!.ItemCount -= 1;
}
}
if (deco.Type != "/Lotus/Objects/Tenno/Props/TnoPaintBotDojoDeco") {
if (!meta || (meta.price == 0 && meta.ingredients.length == 0) || config.noDojoDecoBuildStage) {
deco.CompletionTime = new Date(); deco.CompletionTime = new Date();
if (meta) {
processDojoBuildMaterialsGathered(guild, meta);
}
} else if (guild.AutoContributeFromVault && guild.VaultRegularCredits && guild.VaultMiscItems) {
if (guild.VaultRegularCredits >= scaleRequiredCount(guild.Tier, meta.price)) {
let enoughMiscItems = true;
for (const ingredient of meta.ingredients) {
if (
getVaultMiscItemCount(guild, ingredient.ItemType) <
scaleRequiredCount(guild.Tier, ingredient.ItemCount)
) {
enoughMiscItems = false;
break;
}
}
if (enoughMiscItems) {
guild.VaultRegularCredits -= scaleRequiredCount(guild.Tier, meta.price);
deco.RegularCredits = scaleRequiredCount(guild.Tier, meta.price);
deco.MiscItems = [];
for (const ingredient of meta.ingredients) {
guild.VaultMiscItems.find(x => x.ItemType == ingredient.ItemType)!.ItemCount -=
scaleRequiredCount(guild.Tier, ingredient.ItemCount);
deco.MiscItems.push({
ItemType: ingredient.ItemType,
ItemCount: scaleRequiredCount(guild.Tier, ingredient.ItemCount)
});
}
deco.CompletionTime = new Date(Date.now() + meta.time * 1000);
processDojoBuildMaterialsGathered(guild, meta);
}
}
}
} }
} }
@ -56,4 +114,9 @@ interface IPlaceDecoInComponentRequest {
Pos: number[]; Pos: number[];
Rot: number[]; Rot: number[];
Name?: string; Name?: string;
Sockets?: number;
Scale?: number; // only provided alongside MoveId and seems to always be 1
MoveId?: string;
ShipDeco?: boolean;
VaultDeco?: boolean;
} }

View File

@ -0,0 +1,9 @@
import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const playedParkourTutorialController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
await Inventory.updateOne({ accountOwnerId: accountId }, { PlayedParkourTutorial: true });
res.end();
};

View File

@ -0,0 +1,75 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { GuildAd, GuildMember } from "@/src/models/guildModel";
import {
addGuildMemberMiscItemContribution,
addVaultMiscItems,
getGuildForRequestEx,
getVaultMiscItemCount,
hasGuildPermissionEx
} from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getVendorManifestByTypeName } from "@/src/services/serversideVendorsService";
import { GuildPermission } from "@/src/types/guildTypes";
import { IPurchaseParams } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express";
export const postGuildAdvertisementController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId MiscItems");
const guild = await getGuildForRequestEx(req, inventory);
const guildMember = (await GuildMember.findOne({ accountId, guildId: guild._id }))!;
if (!hasGuildPermissionEx(guild, guildMember, GuildPermission.Advertiser)) {
res.status(400).end();
return;
}
const payload = getJSONfromString<IPostGuildAdvertisementRequest>(String(req.body));
// Handle resource cost
const vendor = getVendorManifestByTypeName(
"/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest"
)!;
const offer = vendor.VendorInfo.ItemManifest.find(x => x.StoreItem == payload.PurchaseParams.StoreItem)!;
if (getVaultMiscItemCount(guild, offer.ItemPrices![0].ItemType) >= offer.ItemPrices![0].ItemCount) {
addVaultMiscItems(guild, [
{
ItemType: offer.ItemPrices![0].ItemType,
ItemCount: offer.ItemPrices![0].ItemCount * -1
}
]);
} else {
const miscItem = inventory.MiscItems.find(x => x.ItemType == offer.ItemPrices![0].ItemType);
if (!miscItem || miscItem.ItemCount < offer.ItemPrices![0].ItemCount) {
res.status(400).json("Insufficient funds");
return;
}
miscItem.ItemCount -= offer.ItemPrices![0].ItemCount;
addGuildMemberMiscItemContribution(guildMember, offer.ItemPrices![0]);
await guildMember.save();
await inventory.save();
}
// Create or update ad
await GuildAd.findOneAndUpdate(
{ GuildId: guild._id },
{
Emblem: guild.Emblem,
Expiry: new Date(Date.now() + 12 * 3600 * 1000),
Features: payload.Features,
GuildName: guild.Name,
MemberCount: await GuildMember.countDocuments({ guildId: guild._id, status: 0 }),
RecruitMsg: payload.RecruitMsg,
Tier: guild.Tier
},
{ upsert: true }
);
res.end();
};
interface IPostGuildAdvertisementRequest {
Features: number;
RecruitMsg: string;
Languages: string[];
PurchaseParams: IPurchaseParams;
}

View File

@ -16,7 +16,7 @@ export const queueDojoComponentDestructionController: RequestHandler = async (re
const componentId = req.query.componentId as string; const componentId = req.query.componentId as string;
guild.DojoComponents.id(componentId)!.DestructionTime = new Date( guild.DojoComponents.id(componentId)!.DestructionTime = new Date(
Date.now() + (config.fastDojoRoomDestruction ? 5_000 : 2 * 3600_000) (Math.trunc(Date.now() / 1000) + (config.fastDojoRoomDestruction ? 5 : 2 * 3600)) * 1000
); );
await guild.save(); await guild.save();

View File

@ -0,0 +1,27 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory, updateCurrency } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const releasePetController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "RegularCredits KubrowPets");
const payload = getJSONfromString<IReleasePetRequest>(String(req.body));
const inventoryChanges = updateCurrency(
inventory,
payload.recipeName == "/Lotus/Types/Game/KubrowPet/ReleasePetRecipe" ? 25000 : 0,
false
);
inventoryChanges.RemovedIdItems = [{ ItemId: { $oid: payload.petId } }];
inventory.KubrowPets.pull({ _id: payload.petId });
await inventory.save();
res.json({ inventoryChanges }); // Not a mistake; it's "inventoryChanges" here.
};
interface IReleasePetRequest {
recipeName: "/Lotus/Types/Game/KubrowPet/ReleasePetRecipe" | "webui";
petId: string;
}

View File

@ -0,0 +1,38 @@
import { AllianceMember, Guild, GuildMember } from "@/src/models/guildModel";
import { deleteAlliance } from "@/src/services/guildService";
import { getAccountForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
export const removeFromAllianceController: RequestHandler = async (req, res) => {
// Check requester is a warlord in their guild
const account = await getAccountForRequest(req);
const guildMember = (await GuildMember.findOne({ accountId: account._id, status: 0 }))!;
if (guildMember.rank > 1) {
res.status(400).json({ Error: 104 });
return;
}
let allianceMember = (await AllianceMember.findOne({ guildId: guildMember.guildId }))!;
if (!guildMember.guildId.equals(req.query.guildId as string)) {
// Removing a guild that is not our own needs additional permissions
if (!(allianceMember.Permissions & GuildPermission.Ruler)) {
res.status(400).json({ Error: 104 });
return;
}
// Update allianceMember to point to the alliance to kick
allianceMember = (await AllianceMember.findOne({ guildId: req.query.guildId }))!;
}
if (allianceMember.Permissions & GuildPermission.Ruler) {
await deleteAlliance(allianceMember.allianceId);
} else {
await Promise.all([
await Guild.updateOne({ _id: allianceMember.guildId }, { $unset: { AllianceId: "" } }),
await AllianceMember.deleteOne({ _id: allianceMember._id })
]);
}
res.end();
};

View File

@ -1,7 +1,8 @@
import { GuildMember } from "@/src/models/guildModel"; import { GuildMember } from "@/src/models/guildModel";
import { Inbox } from "@/src/models/inboxModel"; import { Inbox } from "@/src/models/inboxModel";
import { Account } from "@/src/models/loginModel"; import { Account } from "@/src/models/loginModel";
import { getGuildForRequest, hasGuildPermission } from "@/src/services/guildService"; import { deleteGuild, getGuildForRequest, hasGuildPermission, removeDojoKeyItems } from "@/src/services/guildService";
import { createMessage } from "@/src/services/inboxService";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes"; import { GuildPermission } from "@/src/types/guildTypes";
@ -18,24 +19,34 @@ export const removeFromGuildController: RequestHandler = async (req, res) => {
} }
const guildMember = (await GuildMember.findOne({ accountId: payload.userId, guildId: guild._id }))!; const guildMember = (await GuildMember.findOne({ accountId: payload.userId, guildId: guild._id }))!;
if (guildMember.status == 0) { if (guildMember.rank == 0) {
const inventory = await getInventory(payload.userId); await deleteGuild(guild._id);
inventory.GuildId = undefined;
// Remove clan key or blueprint from kicked member
const itemIndex = inventory.LevelKeys.findIndex(x => x.ItemType == "/Lotus/Types/Keys/DojoKey");
if (itemIndex != -1) {
inventory.LevelKeys.splice(itemIndex, 1);
} else { } else {
const recipeIndex = inventory.Recipes.findIndex(x => x.ItemType == "/Lotus/Types/Keys/DojoKeyBlueprint"); if (guildMember.status == 0) {
if (recipeIndex != -1) { const inventory = await getInventory(payload.userId, "GuildId LevelKeys Recipes");
inventory.Recipes.splice(recipeIndex, 1); inventory.GuildId = undefined;
} removeDojoKeyItems(inventory);
}
await inventory.save(); await inventory.save();
} else if (guildMember.status == 1) {
// TODO: Handle clan leader kicking themselves (guild should be deleted in this case, I think) // TOVERIFY: Is this inbox message actually sent on live?
await createMessage(guildMember.accountId, [
{
sndr: "/Lotus/Language/Bosses/Ordis",
msg: "/Lotus/Language/Clan/RejectedFromClan",
sub: "/Lotus/Language/Clan/RejectedFromClanHeader",
arg: [
{
Key: "PLAYER_NAME",
Tag: (await Account.findOne({ _id: guildMember.accountId }, "DisplayName"))!.DisplayName
},
{
Key: "CLAN_NAME",
Tag: guild.Name
}
]
// TOVERIFY: If this message is sent on live, is it highPriority?
}
]);
} else if (guildMember.status == 2) { } else if (guildMember.status == 2) {
// Delete the inbox message for the invite // Delete the inbox message for the invite
await Inbox.deleteOne({ await Inbox.deleteOne({
@ -62,6 +73,7 @@ export const removeFromGuildController: RequestHandler = async (req, res) => {
}); });
} }
await guild.save(); await guild.save();
}
res.json({ res.json({
_id: payload.userId, _id: payload.userId,

View File

@ -0,0 +1,21 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Account, Ignore } from "@/src/models/loginModel";
import { getAccountForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const removeIgnoredUserController: RequestHandler = async (req, res) => {
const accountId = await getAccountForRequest(req);
const data = getJSONfromString<IRemoveIgnoredUserRequest>(String(req.body));
const ignoreeAccount = await Account.findOne(
{ DisplayName: data.playerName.substring(0, data.playerName.length - 1) },
"_id"
);
if (ignoreeAccount) {
await Ignore.deleteOne({ ignorer: accountId, ignoree: ignoreeAccount._id });
}
res.end();
};
interface IRemoveIgnoredUserRequest {
playerName: string;
}

View File

@ -1,6 +1,9 @@
import { getInventory } from "@/src/services/inventoryService"; import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { config } from "@/src/services/configService";
import { addEmailItem, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { ICompletedDialogue } from "@/src/types/inventoryTypes/inventoryTypes"; import { ICompletedDialogue, IDialogueDatabase } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
@ -8,7 +11,7 @@ export const saveDialogueController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const request = JSON.parse(String(req.body)) as SaveDialogueRequest; const request = JSON.parse(String(req.body)) as SaveDialogueRequest;
if ("YearIteration" in request) { if ("YearIteration" in request) {
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId, "DialogueHistory");
if (inventory.DialogueHistory) { if (inventory.DialogueHistory) {
inventory.DialogueHistory.YearIteration = request.YearIteration; inventory.DialogueHistory.YearIteration = request.YearIteration;
} else { } else {
@ -21,33 +24,25 @@ export const saveDialogueController: RequestHandler = async (req, res) => {
if (!inventory.DialogueHistory) { if (!inventory.DialogueHistory) {
throw new Error("bad inventory state"); throw new Error("bad inventory state");
} }
if (request.QueuedDialogues.length != 0 || request.OtherDialogueInfos.length != 0) { const inventoryChanges: IInventoryChanges = {};
logger.error(`saveDialogue request not fully handled: ${String(req.body)}`); const tomorrowAt0Utc = config.noKimCooldowns
} ? Date.now()
: (Math.trunc(Date.now() / 86400_000) + 1) * 86400_000;
inventory.DialogueHistory.Dialogues ??= []; inventory.DialogueHistory.Dialogues ??= [];
let dialogue = inventory.DialogueHistory.Dialogues.find(x => x.DialogueName == request.DialogueName); const dialogue = getDialogue(inventory, request.DialogueName);
if (!dialogue) {
dialogue =
inventory.DialogueHistory.Dialogues[
inventory.DialogueHistory.Dialogues.push({
Rank: 0,
Chemistry: 0,
AvailableDate: new Date(0),
AvailableGiftDate: new Date(0),
RankUpExpiry: new Date(0),
BountyChemExpiry: new Date(0),
Gifts: [],
Booleans: [],
Completed: [],
DialogueName: request.DialogueName
}) - 1
];
}
dialogue.Rank = request.Rank; dialogue.Rank = request.Rank;
dialogue.Chemistry = request.Chemistry; dialogue.Chemistry = request.Chemistry;
//dialogue.QueuedDialogues = request.QueuedDialogues; if (request.Data) {
dialogue.QueuedDialogues = request.QueuedDialogues;
for (const bool of request.Booleans) { for (const bool of request.Booleans) {
dialogue.Booleans.push(bool); dialogue.Booleans.push(bool);
if (bool == "LizzieShawzin") {
await addEmailItem(
inventory,
"/Lotus/Types/Items/EmailItems/LizzieShawzinSkinEmailItem",
inventoryChanges
);
}
} }
for (const bool of request.ResetBooleans) { for (const bool of request.ResetBooleans) {
const index = dialogue.Booleans.findIndex(x => x == bool); const index = dialogue.Booleans.findIndex(x => x == bool);
@ -56,13 +51,36 @@ export const saveDialogueController: RequestHandler = async (req, res) => {
} }
} }
dialogue.Completed.push(request.Data); dialogue.Completed.push(request.Data);
const tomorrowAt0Utc = (Math.trunc(Date.now() / (86400 * 1000)) + 1) * 86400 * 1000;
dialogue.AvailableDate = new Date(tomorrowAt0Utc); dialogue.AvailableDate = new Date(tomorrowAt0Utc);
for (const info of request.OtherDialogueInfos) {
const otherDialogue = getDialogue(inventory, info.Dialogue);
if (info.Tag != "") {
otherDialogue.QueuedDialogues.push(info.Tag);
}
otherDialogue.Chemistry += info.Value; // unsure
}
await inventory.save(); await inventory.save();
res.json({ res.json({
InventoryChanges: [], InventoryChanges: inventoryChanges,
AvailableDate: { $date: { $numberLong: tomorrowAt0Utc.toString() } } AvailableDate: { $date: { $numberLong: tomorrowAt0Utc.toString() } }
}); });
} else if (request.Gift) {
const inventoryChanges = updateCurrency(inventory, request.Gift.Cost, false);
const gift = dialogue.Gifts.find(x => x.Item == request.Gift!.Item);
if (gift) {
gift.GiftedQuantity += 1;
} else {
dialogue.Gifts.push({ Item: request.Gift.Item, GiftedQuantity: 1 });
}
dialogue.AvailableGiftDate = new Date(tomorrowAt0Utc);
await inventory.save();
res.json({
InventoryChanges: inventoryChanges,
AvailableGiftDate: { $date: { $numberLong: tomorrowAt0Utc.toString() } }
});
} else {
logger.error(`saveDialogue request not fully handled: ${String(req.body)}`);
}
} }
}; };
@ -77,11 +95,17 @@ interface SaveCompletedDialogueRequest {
Rank: number; Rank: number;
Chemistry: number; Chemistry: number;
CompletionType: number; CompletionType: number;
QueuedDialogues: string[]; // unsure QueuedDialogues: string[];
Gift?: {
Item: string;
GainedChemistry: number;
Cost: number;
GiftedQuantity: number;
};
Booleans: string[]; Booleans: string[];
ResetBooleans: string[]; ResetBooleans: string[];
Data: ICompletedDialogue; Data?: ICompletedDialogue;
OtherDialogueInfos: IOtherDialogueInfo[]; // unsure OtherDialogueInfos: IOtherDialogueInfo[];
} }
interface IOtherDialogueInfo { interface IOtherDialogueInfo {
@ -89,3 +113,26 @@ interface IOtherDialogueInfo {
Tag: string; Tag: string;
Value: number; Value: number;
} }
const getDialogue = (inventory: TInventoryDatabaseDocument, dialogueName: string): IDialogueDatabase => {
let dialogue = inventory.DialogueHistory!.Dialogues!.find(x => x.DialogueName == dialogueName);
if (!dialogue) {
dialogue =
inventory.DialogueHistory!.Dialogues![
inventory.DialogueHistory!.Dialogues!.push({
Rank: 0,
Chemistry: 0,
AvailableDate: new Date(0),
AvailableGiftDate: new Date(0),
RankUpExpiry: new Date(0),
BountyChemExpiry: new Date(0),
QueuedDialogues: [],
Gifts: [],
Booleans: [],
Completed: [],
DialogueName: dialogueName
}) - 1
];
}
return dialogue;
};

View File

@ -1,32 +0,0 @@
import { RequestHandler } from "express";
import { ISaveLoadoutRequest } from "@/src/types/saveLoadoutTypes";
import { handleInventoryItemConfigChange } from "@/src/services/saveLoadoutService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { logger } from "@/src/utils/logger";
export const saveLoadoutController: RequestHandler = async (req, res) => {
//validate here
const accountId = await getAccountIdForRequest(req);
try {
const body: ISaveLoadoutRequest = JSON.parse(req.body as string) as ISaveLoadoutRequest;
// console.log(util.inspect(body, { showHidden: false, depth: null, colors: true }));
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { UpgradeVer, ...equipmentChanges } = body;
const newLoadoutId = await handleInventoryItemConfigChange(equipmentChanges, accountId);
//send back new loadout id, if new loadout was added
if (newLoadoutId) {
res.send(newLoadoutId);
}
res.status(200).end();
} catch (error: unknown) {
if (error instanceof Error) {
logger.error(`error in saveLoadoutController: ${error.message}`);
res.status(400).json({ error: error.message });
} else {
res.status(400).json({ error: "unknown error" });
}
}
};

View File

@ -0,0 +1,21 @@
import { RequestHandler } from "express";
import { ISaveLoadoutRequest } from "@/src/types/saveLoadoutTypes";
import { handleInventoryItemConfigChange } from "@/src/services/saveLoadoutService";
import { getAccountIdForRequest } from "@/src/services/loginService";
export const saveLoadoutController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const body: ISaveLoadoutRequest = JSON.parse(req.body as string) as ISaveLoadoutRequest;
// console.log(util.inspect(body, { showHidden: false, depth: null, colors: true }));
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { UpgradeVer, ...equipmentChanges } = body;
const newLoadoutId = await handleInventoryItemConfigChange(equipmentChanges, accountId);
//send back new loadout id, if new loadout was added
if (newLoadoutId) {
res.send(newLoadoutId);
}
res.end();
};

View File

@ -0,0 +1,25 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Guild } from "@/src/models/guildModel";
import { hasGuildPermission } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
export const saveVaultAutoContributeController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId");
const guild = (await Guild.findById(inventory.GuildId!, "Ranks AutoContributeFromVault"))!;
if (!(await hasGuildPermission(guild, accountId, GuildPermission.Treasurer))) {
res.status(400).send("Invalid permission").end();
return;
}
const data = getJSONfromString<ISetVaultAutoContributeRequest>(String(req.body));
guild.AutoContributeFromVault = data.autoContributeFromVault;
await guild.save();
res.end();
};
interface ISetVaultAutoContributeRequest {
autoContributeFromVault: boolean;
}

View File

@ -6,14 +6,64 @@ import {
addRecipes, addRecipes,
addMiscItems, addMiscItems,
addConsumables, addConsumables,
freeUpSlot freeUpSlot,
combineInventoryChanges,
addCrewShipRawSalvage
} from "@/src/services/inventoryService"; } from "@/src/services/inventoryService";
import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; 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";
export const sellController: RequestHandler = async (req, res) => { export const sellController: RequestHandler = async (req, res) => {
const payload = JSON.parse(String(req.body)) as ISellRequest; const payload = JSON.parse(String(req.body)) as ISellRequest;
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId); const requiredFields = new Set<keyof TInventoryDatabaseDocument>();
if (payload.SellCurrency == "SC_RegularCredits") {
requiredFields.add("RegularCredits");
} else if (payload.SellCurrency == "SC_FusionPoints") {
requiredFields.add("FusionPoints");
} else {
requiredFields.add("MiscItems");
}
for (const key of Object.keys(payload.Items)) {
requiredFields.add(key as keyof TInventoryDatabaseDocument);
}
if (requiredFields.has("Upgrades")) {
requiredFields.add("RawUpgrades");
}
if (payload.Items.Suits) {
requiredFields.add(InventorySlot.SUITS);
}
if (payload.Items.LongGuns || payload.Items.Pistols || payload.Items.Melee) {
requiredFields.add(InventorySlot.WEAPONS);
}
if (payload.Items.SpaceSuits) {
requiredFields.add(InventorySlot.SPACESUITS);
}
if (payload.Items.SpaceGuns || payload.Items.SpaceMelee) {
requiredFields.add(InventorySlot.SPACEWEAPONS);
}
if (payload.Items.Sentinels || payload.Items.SentinelWeapons) {
requiredFields.add(InventorySlot.SENTINELS);
}
if (payload.Items.OperatorAmps) {
requiredFields.add(InventorySlot.AMPS);
}
if (payload.Items.Hoverboards) {
requiredFields.add(InventorySlot.SPACESUITS);
}
if (payload.Items.CrewShipWeapons || payload.Items.CrewShipWeaponSkins) {
requiredFields.add(InventorySlot.RJ_COMPONENT_AND_ARMAMENTS);
requiredFields.add("CrewShipRawSalvage");
if (payload.Items.CrewShipWeapons) {
requiredFields.add("CrewShipSalvagedWeapons");
}
if (payload.Items.CrewShipWeaponSkins) {
requiredFields.add("CrewShipSalvagedWeaponSkins");
}
}
const inventory = await getInventory(accountId, Array.from(requiredFields).join(" "));
// Give currency // Give currency
if (payload.SellCurrency == "SC_RegularCredits") { if (payload.SellCurrency == "SC_RegularCredits") {
@ -34,10 +84,14 @@ export const sellController: RequestHandler = async (req, res) => {
ItemCount: payload.SellPrice ItemCount: payload.SellPrice
} }
]); ]);
} else if (payload.SellCurrency == "SC_Resources") {
// Will add appropriate MiscItems from CrewShipWeapons or CrewShipWeaponSkins
} else { } else {
throw new Error("Unknown SellCurrency: " + payload.SellCurrency); throw new Error("Unknown SellCurrency: " + payload.SellCurrency);
} }
const inventoryChanges: IInventoryChanges = {};
// Remove item(s) // Remove item(s)
if (payload.Items.Suits) { if (payload.Items.Suits) {
payload.Items.Suits.forEach(sellItem => { payload.Items.Suits.forEach(sellItem => {
@ -110,6 +164,56 @@ export const sellController: RequestHandler = async (req, res) => {
inventory.Drones.pull({ _id: sellItem.String }); inventory.Drones.pull({ _id: sellItem.String });
}); });
} }
if (payload.Items.CrewShipWeapons) {
payload.Items.CrewShipWeapons.forEach(sellItem => {
if (sellItem.String[0] == "/") {
addCrewShipRawSalvage(inventory, [
{
ItemType: sellItem.String,
ItemCount: sellItem.Count * -1
}
]);
} else {
const index = inventory.CrewShipWeapons.findIndex(x => x._id.equals(sellItem.String));
if (index != -1) {
if (payload.SellCurrency == "SC_Resources") {
refundPartialBuildCosts(inventory, inventory.CrewShipWeapons[index].ItemType, inventoryChanges);
}
inventory.CrewShipWeapons.splice(index, 1);
freeUpSlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS);
} else {
inventory.CrewShipSalvagedWeapons.pull({ _id: sellItem.String });
}
}
});
}
if (payload.Items.CrewShipWeaponSkins) {
payload.Items.CrewShipWeaponSkins.forEach(sellItem => {
if (sellItem.String[0] == "/") {
addCrewShipRawSalvage(inventory, [
{
ItemType: sellItem.String,
ItemCount: sellItem.Count * -1
}
]);
} else {
const index = inventory.CrewShipWeaponSkins.findIndex(x => x._id.equals(sellItem.String));
if (index != -1) {
if (payload.SellCurrency == "SC_Resources") {
refundPartialBuildCosts(
inventory,
inventory.CrewShipWeaponSkins[index].ItemType,
inventoryChanges
);
}
inventory.CrewShipWeaponSkins.splice(index, 1);
freeUpSlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS);
} else {
inventory.CrewShipSalvagedWeaponSkins.pull({ _id: sellItem.String });
}
}
});
}
if (payload.Items.Consumables) { if (payload.Items.Consumables) {
const consumablesChanges = []; const consumablesChanges = [];
for (const sellItem of payload.Items.Consumables) { for (const sellItem of payload.Items.Consumables) {
@ -156,7 +260,9 @@ export const sellController: RequestHandler = async (req, res) => {
} }
await inventory.save(); await inventory.save();
res.json({}); res.json({
inventoryChanges: inventoryChanges // "inventoryChanges" for this response instead of the usual "InventoryChanges"
});
}; };
interface ISellRequest { interface ISellRequest {
@ -177,6 +283,8 @@ interface ISellRequest {
OperatorAmps?: ISellItem[]; OperatorAmps?: ISellItem[];
Hoverboards?: ISellItem[]; Hoverboards?: ISellItem[];
Drones?: ISellItem[]; Drones?: ISellItem[];
CrewShipWeapons?: ISellItem[];
CrewShipWeaponSkins?: ISellItem[];
}; };
SellPrice: number; SellPrice: number;
SellCurrency: SellCurrency:
@ -193,3 +301,33 @@ interface ISellItem {
String: string; // oid or uniqueName String: string; // oid or uniqueName
Count: number; Count: number;
} }
const refundPartialBuildCosts = (
inventory: TInventoryDatabaseDocument,
itemType: string,
inventoryChanges: IInventoryChanges
): void => {
// House versions
const research = Object.values(ExportDojoRecipes.research).find(x => x.resultType == itemType);
if (research) {
const miscItemChanges = research.ingredients.map(x => ({
ItemType: x.ItemType,
ItemCount: Math.trunc(x.ItemCount * 0.8)
}));
addMiscItems(inventory, miscItemChanges);
combineInventoryChanges(inventoryChanges, { MiscItems: miscItemChanges });
return;
}
// Sigma versions
const recipe = Object.values(ExportDojoRecipes.fabrications).find(x => x.resultType == itemType);
if (recipe) {
const miscItemChanges = recipe.ingredients.map(x => ({
ItemType: x.ItemType,
ItemCount: Math.trunc(x.ItemCount * 0.8)
}));
addMiscItems(inventory, miscItemChanges);
combineInventoryChanges(inventoryChanges, { MiscItems: miscItemChanges });
return;
}
};

View File

@ -0,0 +1,31 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { createMessage } from "@/src/services/inboxService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const sendMsgToInBoxController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const data = getJSONfromString<ISendMsgToInBoxRequest>(String(req.body));
await createMessage(accountId, [
{
sub: data.title,
msg: data.message,
sndr: data.sender ?? "/Lotus/Language/Bosses/Ordis",
icon: data.senderIcon,
highPriority: data.highPriority,
transmission: data.transmission,
att: data.attachments
}
]);
res.end();
};
interface ISendMsgToInBoxRequest {
title: string;
message: string;
sender?: string;
senderIcon?: string;
highPriority?: boolean;
transmission?: string;
attachments?: string[];
}

View File

@ -0,0 +1,38 @@
import { AllianceMember, GuildMember } from "@/src/models/guildModel";
import { getAccountForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
export const setAllianceGuildPermissionsController: RequestHandler = async (req, res) => {
// Check requester is a warlord in their guild
const account = await getAccountForRequest(req);
const guildMember = (await GuildMember.findOne({ accountId: account._id, status: 0 }))!;
if (guildMember.rank > 1) {
res.status(400).end();
return;
}
// Check guild is the creator of the alliance and don't allow changing of own permissions. (Technically changing permissions requires the Promoter permission, but both are exclusive to the creator guild.)
const allianceMember = (await AllianceMember.findOne({
guildId: guildMember.guildId,
Pending: false
}))!;
if (
!(allianceMember.Permissions & GuildPermission.Ruler) ||
allianceMember.guildId.equals(req.query.guildId as string)
) {
res.status(400).end();
return;
}
const targetAllianceMember = (await AllianceMember.findOne({
allianceId: allianceMember.allianceId,
guildId: req.query.guildId
}))!;
targetAllianceMember.Permissions =
parseInt(req.query.perms as string) &
(GuildPermission.Recruiter | GuildPermission.Treasurer | GuildPermission.ChatModerator);
await targetAllianceMember.save();
res.end();
};

View File

@ -0,0 +1,34 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
export const setDojoComponentColorsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId LevelKeys");
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Decorator))) {
res.json({ DojoRequestStatus: -1 });
return;
}
const data = getJSONfromString<ISetDojoComponentColorsRequest>(String(req.body));
const component = guild.DojoComponents.id(data.ComponentId)!;
//const deco = component.Decos!.find(x => x._id.equals(data.DecoId))!;
//deco.Pending = true;
//component.PaintBot = new Types.ObjectId(data.DecoId);
if ("lights" in req.query) {
component.PendingLights = data.Colours;
} else {
component.PendingColors = data.Colours;
}
await guild.save();
res.json(await getDojoClient(guild, 0, component._id));
};
interface ISetDojoComponentColorsRequest {
ComponentId: string;
DecoId: string;
Colours: number[];
}

View File

@ -0,0 +1,25 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getDojoClient, getGuildForRequestEx, hasAccessToDojo, hasGuildPermission } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
export const setDojoComponentSettingsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "GuildId LevelKeys");
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Decorator))) {
res.json({ DojoRequestStatus: -1 });
return;
}
const component = guild.DojoComponents.id(req.query.componentId)!;
const data = getJSONfromString<ISetDojoComponentSettingsRequest>(String(req.body));
component.Settings = data.Settings;
await guild.save();
res.json(await getDojoClient(guild, 0, component._id));
};
interface ISetDojoComponentSettingsRequest {
Settings: string;
}

View File

@ -1,21 +1,44 @@
import { Guild } from "@/src/models/guildModel"; import { Alliance, Guild, GuildMember } from "@/src/models/guildModel";
import { hasGuildPermission } from "@/src/services/guildService"; import { hasGuildPermissionEx } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService"; import { getAccountForRequest, getSuffixedName } from "@/src/services/loginService";
import { GuildPermission } from "@/src/types/guildTypes"; import { GuildPermission, ILongMOTD } from "@/src/types/guildTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
export const setGuildMotdController: RequestHandler = async (req, res) => { export const setGuildMotdController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req); const account = await getAccountForRequest(req);
const inventory = await getInventory(account._id.toString(), "GuildId"); const inventory = await getInventory(account._id.toString(), "GuildId");
const guild = (await Guild.findById(inventory.GuildId!))!; const guild = (await Guild.findById(inventory.GuildId!))!;
if (!(await hasGuildPermission(guild, account._id, GuildPermission.Herald))) { const member = (await GuildMember.findOne({ accountId: account._id, guildId: guild._id }))!;
const IsLongMOTD = "longMOTD" in req.query;
const MOTD = req.body ? String(req.body) : undefined;
if ("alliance" in req.query) {
if (member.rank > 1) {
res.status(400).json("Invalid permission"); res.status(400).json("Invalid permission");
return; return;
} }
const IsLongMOTD = "longMOTD" in req.query; const alliance = (await Alliance.findById(guild.AllianceId!))!;
const MOTD = req.body ? String(req.body) : undefined; const motd = MOTD
? ({
message: MOTD,
authorName: getSuffixedName(account),
authorGuildName: guild.Name
} satisfies ILongMOTD)
: undefined;
if (IsLongMOTD) {
alliance.LongMOTD = motd;
} else {
alliance.MOTD = motd;
}
await alliance.save();
} else {
if (!hasGuildPermissionEx(guild, member, GuildPermission.Herald)) {
res.status(400).json("Invalid permission");
return;
}
if (IsLongMOTD) { if (IsLongMOTD) {
if (MOTD) { if (MOTD) {
@ -30,6 +53,7 @@ export const setGuildMotdController: RequestHandler = async (req, res) => {
guild.MOTD = MOTD ?? ""; guild.MOTD = MOTD ?? "";
} }
await guild.save(); await guild.save();
}
res.json({ IsLongMOTD, MOTD }); res.json({ IsLongMOTD, MOTD });
}; };

View File

@ -0,0 +1,21 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IHubNpcCustomization } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
export const setHubNpcCustomizationsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "HubNpcCustomizations");
const upload = getJSONfromString<IHubNpcCustomization>(String(req.body));
inventory.HubNpcCustomizations ??= [];
const cust = inventory.HubNpcCustomizations.find(x => x.Tag == upload.Tag);
if (cust) {
cust.Colors = upload.Colors;
cust.Pattern = upload.Pattern;
} else {
inventory.HubNpcCustomizations.push(upload);
}
await inventory.save();
res.end();
};

View File

@ -1,5 +1,5 @@
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { ISetPlacedDecoInfoRequest } from "@/src/types/shipTypes"; import { IPictureFrameInfo, ISetPlacedDecoInfoRequest } from "@/src/types/shipTypes";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { handleSetPlacedDecoInfo } from "@/src/services/shipCustomizationsService"; import { handleSetPlacedDecoInfo } from "@/src/services/shipCustomizationsService";
@ -7,5 +7,17 @@ export const setPlacedDecoInfoController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const payload = JSON.parse(req.body as string) as ISetPlacedDecoInfoRequest; const payload = JSON.parse(req.body as string) as ISetPlacedDecoInfoRequest;
await handleSetPlacedDecoInfo(accountId, payload); await handleSetPlacedDecoInfo(accountId, payload);
res.end(); res.json({
DecoId: payload.DecoId,
IsPicture: true,
PictureFrameInfo: payload.PictureFrameInfo,
BootLocation: payload.BootLocation
} satisfies ISetPlacedDecoInfoResponse);
}; };
interface ISetPlacedDecoInfoResponse {
DecoId: string;
IsPicture: boolean;
PictureFrameInfo?: IPictureFrameInfo;
BootLocation?: string;
}

View File

@ -3,29 +3,40 @@ import { RequestHandler } from "express";
import { getPersonalRooms } from "@/src/services/personalRoomsService"; import { getPersonalRooms } from "@/src/services/personalRoomsService";
import { IOid } from "@/src/types/commonTypes"; import { IOid } from "@/src/types/commonTypes";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { IFavouriteLoadoutDatabase, TBootLocation } from "@/src/types/shipTypes";
export const setShipFavouriteLoadoutController: RequestHandler = async (req, res) => { export const setShipFavouriteLoadoutController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const personalRooms = await getPersonalRooms(accountId); const personalRooms = await getPersonalRooms(accountId);
const body = JSON.parse(String(req.body)) as ISetShipFavouriteLoadoutRequest; const body = JSON.parse(String(req.body)) as ISetShipFavouriteLoadoutRequest;
if (body.BootLocation != "SHOP") { if (body.BootLocation == "LISET") {
throw new Error(`unexpected BootLocation: ${body.BootLocation}`); personalRooms.Ship.FavouriteLoadoutId = new Types.ObjectId(body.FavouriteLoadoutId.$oid);
} } else if (body.BootLocation == "APARTMENT") {
const display = personalRooms.TailorShop.FavouriteLoadouts.find(x => x.Tag == body.TagName); updateTaggedDisplay(personalRooms.Apartment.FavouriteLoadouts, body);
if (display) { } else if (body.BootLocation == "SHOP") {
display.LoadoutId = new Types.ObjectId(body.FavouriteLoadoutId.$oid); updateTaggedDisplay(personalRooms.TailorShop.FavouriteLoadouts, body);
} else { } else {
personalRooms.TailorShop.FavouriteLoadouts.push({ console.log(body);
Tag: body.TagName, throw new Error(`unexpected BootLocation: ${body.BootLocation}`);
LoadoutId: new Types.ObjectId(body.FavouriteLoadoutId.$oid)
});
} }
await personalRooms.save(); await personalRooms.save();
res.json({}); res.json({});
}; };
interface ISetShipFavouriteLoadoutRequest { interface ISetShipFavouriteLoadoutRequest {
BootLocation: string; BootLocation: TBootLocation;
FavouriteLoadoutId: IOid; FavouriteLoadoutId: IOid;
TagName: string; TagName?: string;
} }
const updateTaggedDisplay = (arr: IFavouriteLoadoutDatabase[], body: ISetShipFavouriteLoadoutRequest): void => {
const display = arr.find(x => x.Tag == body.TagName!);
if (display) {
display.LoadoutId = new Types.ObjectId(body.FavouriteLoadoutId.$oid);
} else {
arr.push({
Tag: body.TagName!,
LoadoutId: new Types.ObjectId(body.FavouriteLoadoutId.$oid)
});
}
};

View File

@ -0,0 +1,48 @@
import { addMiscItems, combineInventoryChanges, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getPersonalRooms } from "@/src/services/personalRoomsService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express";
export const setShipVignetteController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "MiscItems");
const personalRooms = await getPersonalRooms(accountId);
const body = JSON.parse(String(req.body)) as ISetShipVignetteRequest;
personalRooms.Ship.Wallpaper = body.Wallpaper;
personalRooms.Ship.Vignette = body.Vignette;
personalRooms.Ship.VignetteFish ??= [];
const inventoryChanges: IInventoryChanges = {};
for (let i = 0; i != body.Fish.length; ++i) {
if (body.Fish[i] && !personalRooms.Ship.VignetteFish[i]) {
logger.debug(`moving ${body.Fish[i]} from inventory to vignette slot ${i}`);
const miscItemsDelta = [{ ItemType: body.Fish[i], ItemCount: -1 }];
addMiscItems(inventory, miscItemsDelta);
combineInventoryChanges(inventoryChanges, { MiscItems: miscItemsDelta });
} else if (personalRooms.Ship.VignetteFish[i] && !body.Fish[i]) {
logger.debug(`moving ${personalRooms.Ship.VignetteFish[i]} from vignette slot ${i} to inventory`);
const miscItemsDelta = [{ ItemType: personalRooms.Ship.VignetteFish[i], ItemCount: +1 }];
addMiscItems(inventory, miscItemsDelta);
combineInventoryChanges(inventoryChanges, { MiscItems: miscItemsDelta });
}
}
personalRooms.Ship.VignetteFish = body.Fish;
if (body.VignetteDecos.length) {
logger.error(`setShipVignette request not fully handled:`, body);
}
await Promise.all([inventory.save(), personalRooms.save()]);
res.json({
Wallpaper: body.Wallpaper,
Vignette: body.Vignette,
VignetteFish: body.Fish,
InventoryChanges: inventoryChanges
});
};
interface ISetShipVignetteRequest {
Wallpaper: string;
Vignette: string;
Fish: string[];
VignetteDecos: unknown[];
}

View File

@ -1,20 +1,25 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { WeaponTypeInternal } from "@/src/services/itemDataService"; import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
import { equipmentKeys, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
export const setWeaponSkillTreeController: RequestHandler = async (req, res) => { export const setWeaponSkillTreeController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const payload = getJSONfromString<ISetWeaponSkillTreeRequest>(String(req.body)); const payload = getJSONfromString<ISetWeaponSkillTreeRequest>(String(req.body));
const item = inventory[req.query.Category as WeaponTypeInternal].find( if (equipmentKeys.indexOf(req.query.Category as TEquipmentKey) != -1) {
item => item._id.toString() == (req.query.ItemId as string) await Inventory.updateOne(
)!; {
item.SkillTree = payload.SkillTree; accountOwnerId: accountId,
[`${req.query.Category as string}._id`]: req.query.ItemId as string
},
{
[`${req.query.Category as string}.$.SkillTree`]: payload.SkillTree
}
);
}
await inventory.save();
res.end(); res.end();
}; };

View File

@ -90,7 +90,6 @@ export const startRecipeController: RequestHandler = async (req, res) => {
spectreLoadout.LongGuns = item.ItemType; spectreLoadout.LongGuns = item.ItemType;
spectreLoadout.LongGunsModularParts = item.ModularParts; spectreLoadout.LongGunsModularParts = item.ModularParts;
} else { } else {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
console.assert(type == "/Lotus/Types/Game/LotusMeleeWeapon"); console.assert(type == "/Lotus/Types/Game/LotusMeleeWeapon");
const item = inventory.Melee.id(oid)!; const item = inventory.Melee.id(oid)!;
spectreLoadout.Melee = item.ItemType; spectreLoadout.Melee = item.ItemType;
@ -111,6 +110,8 @@ export const startRecipeController: RequestHandler = async (req, res) => {
inventory.PendingSpectreLoadouts.push(spectreLoadout); inventory.PendingSpectreLoadouts.push(spectreLoadout);
logger.debug("pending spectre loadout", spectreLoadout); logger.debug("pending spectre loadout", spectreLoadout);
} }
} else if (recipe.secretIngredientAction == "SIA_UNBRAND") {
pr.SuitToUnbrand = new Types.ObjectId(startRecipeRequest.Ids[recipe.ingredients.length + 0]);
} }
await inventory.save(); await inventory.save();

View File

@ -3,15 +3,9 @@ import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { ExportNightwave, ExportSyndicates, ISyndicateSacrifice } from "warframe-public-export-plus"; import { ExportNightwave, ExportSyndicates, ISyndicateSacrifice } from "warframe-public-export-plus";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService"; import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { import { addMiscItems, combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService";
addItem,
addMiscItems,
combineInventoryChanges,
getInventory,
updateCurrency
} from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { fromStoreItem, isStoreItem } from "@/src/services/itemDataService"; import { isStoreItem, toStoreItem } from "@/src/services/itemDataService";
export const syndicateSacrificeController: RequestHandler = async (request, response) => { export const syndicateSacrificeController: RequestHandler = async (request, response) => {
const accountId = await getAccountIdForRequest(request); const accountId = await getAccountIdForRequest(request);
@ -57,7 +51,7 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp
syndicate.Title ??= 0; syndicate.Title ??= 0;
syndicate.Title += 1; syndicate.Title += 1;
if (syndicate.Title > 0 && manifest.favours.length != 0) { if (syndicate.Title > 0 && manifest.favours.find(x => x.rankUpReward && x.requiredLevel == syndicate.Title)) {
syndicate.FreeFavorsEarned ??= []; syndicate.FreeFavorsEarned ??= [];
if (!syndicate.FreeFavorsEarned.includes(syndicate.Title)) { if (!syndicate.FreeFavorsEarned.includes(syndicate.Title)) {
syndicate.FreeFavorsEarned.push(syndicate.Title); syndicate.FreeFavorsEarned.push(syndicate.Title);
@ -77,10 +71,13 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp
res.NewEpisodeReward = true; res.NewEpisodeReward = true;
const reward = ExportNightwave.rewards[index]; const reward = ExportNightwave.rewards[index];
let rewardType = reward.uniqueName; let rewardType = reward.uniqueName;
if (isStoreItem(rewardType)) { if (!isStoreItem(rewardType)) {
rewardType = fromStoreItem(rewardType); rewardType = toStoreItem(rewardType);
} }
combineInventoryChanges(res.InventoryChanges, await addItem(inventory, rewardType, reward.itemCount ?? 1)); combineInventoryChanges(
res.InventoryChanges,
(await handleStoreItemAcquisition(rewardType, inventory, reward.itemCount)).InventoryChanges
);
} }
} }

View File

@ -1,16 +1,9 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { import { addMiscItems, addStanding, freeUpSlot, getInventory } from "@/src/services/inventoryService";
addMiscItems,
freeUpSlot,
getInventory,
getStandingLimit,
updateStandingLimit
} from "@/src/services/inventoryService";
import { IMiscItem, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes"; import { IMiscItem, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { IOid } from "@/src/types/commonTypes"; import { IOid } from "@/src/types/commonTypes";
import { ExportSyndicates, ExportWeapons } from "warframe-public-export-plus"; import { ExportSyndicates, ExportWeapons } from "warframe-public-export-plus";
import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes";
@ -61,38 +54,13 @@ export const syndicateStandingBonusController: RequestHandler = async (req, res)
inventoryChanges[slotBin] = { count: -1, platinum: 0, Slots: 1 }; inventoryChanges[slotBin] = { count: -1, platinum: 0, Slots: 1 };
} }
let syndicate = inventory.Affiliations.find(x => x.Tag == request.Operation.AffiliationTag); const affiliationMod = addStanding(inventory, request.Operation.AffiliationTag, gainedStanding, true);
if (!syndicate) {
syndicate =
inventory.Affiliations[
inventory.Affiliations.push({ Tag: request.Operation.AffiliationTag, Standing: 0 }) - 1
];
}
const max = getMaxStanding(syndicateMeta, syndicate.Title ?? 0);
if (syndicate.Standing + gainedStanding > max) {
gainedStanding = max - syndicate.Standing;
}
if (syndicateMeta.medallionsCappedByDailyLimit) {
if (gainedStanding > getStandingLimit(inventory, syndicateMeta.dailyLimitBin)) {
gainedStanding = getStandingLimit(inventory, syndicateMeta.dailyLimitBin);
}
updateStandingLimit(inventory, syndicateMeta.dailyLimitBin, gainedStanding);
}
syndicate.Standing += gainedStanding;
await inventory.save(); await inventory.save();
res.json({ res.json({
InventoryChanges: inventoryChanges, InventoryChanges: inventoryChanges,
AffiliationMods: [ AffiliationMods: [affiliationMod]
{
Tag: request.Operation.AffiliationTag,
Standing: gainedStanding
}
]
}); });
}; };

View File

@ -6,6 +6,7 @@ import { RequestHandler } from "express";
import { unixTimesInMs } from "@/src/constants/timeConstants"; import { unixTimesInMs } from "@/src/constants/timeConstants";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { createMessage } from "@/src/services/inboxService"; import { createMessage } from "@/src/services/inboxService";
import { config } from "@/src/services/configService";
interface ITrainingResultsRequest { interface ITrainingResultsRequest {
numLevelsGained: number; numLevelsGained: number;
@ -22,11 +23,17 @@ const trainingResultController: RequestHandler = async (req, res): Promise<void>
const trainingResults = getJSONfromString<ITrainingResultsRequest>(String(req.body)); const trainingResults = getJSONfromString<ITrainingResultsRequest>(String(req.body));
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId, "TrainingDate PlayerLevel TradesRemaining");
if (trainingResults.numLevelsGained == 1) { if (trainingResults.numLevelsGained == 1) {
inventory.TrainingDate = new Date(Date.now() + unixTimesInMs.hour * 23); let time = Date.now();
if (!config.noMasteryRankUpCooldown) {
time += unixTimesInMs.hour * 23;
}
inventory.TrainingDate = new Date(time);
inventory.PlayerLevel += 1; inventory.PlayerLevel += 1;
inventory.TradesRemaining += 1;
await createMessage(accountId, [ await createMessage(accountId, [
{ {

View File

@ -3,7 +3,6 @@ import { updateShipFeature } from "@/src/services/personalRoomsService";
import { IUnlockShipFeatureRequest } from "@/src/types/requestTypes"; import { IUnlockShipFeatureRequest } from "@/src/types/requestTypes";
import { parseString } from "@/src/helpers/general"; import { parseString } from "@/src/helpers/general";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const unlockShipFeatureController: RequestHandler = async (req, res) => { export const unlockShipFeatureController: RequestHandler = async (req, res) => {
const accountId = parseString(req.query.accountId); const accountId = parseString(req.query.accountId);
const shipFeatureRequest = JSON.parse((req.body as string).toString()) as IUnlockShipFeatureRequest; const shipFeatureRequest = JSON.parse((req.body as string).toString()) as IUnlockShipFeatureRequest;

View File

@ -1,10 +1,8 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { addChallenges, addSeasonalChallengeHistory, getInventory } from "@/src/services/inventoryService"; import { addChallenges, getInventory } from "@/src/services/inventoryService";
import { IChallengeProgress, ISeasonChallenge } from "@/src/types/inventoryTypes/inventoryTypes"; import { IChallengeProgress, ISeasonChallenge } from "@/src/types/inventoryTypes/inventoryTypes";
import { ExportNightwave } from "warframe-public-export-plus";
import { logger } from "@/src/utils/logger";
import { IAffiliationMods } from "@/src/types/purchaseTypes"; import { IAffiliationMods } from "@/src/types/purchaseTypes";
export const updateChallengeProgressController: RequestHandler = async (req, res) => { export const updateChallengeProgressController: RequestHandler = async (req, res) => {
@ -12,41 +10,19 @@ export const updateChallengeProgressController: RequestHandler = async (req, res
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "ChallengeProgress SeasonChallengeHistory Affiliations"); const inventory = await getInventory(accountId, "ChallengeProgress SeasonChallengeHistory Affiliations");
let affiliationMods: IAffiliationMods[] = [];
if (challenges.ChallengeProgress) { if (challenges.ChallengeProgress) {
addChallenges(inventory, challenges.ChallengeProgress); affiliationMods = addChallenges(inventory, challenges.ChallengeProgress, challenges.SeasonChallengeCompletions);
} }
if (challenges.SeasonChallengeHistory) { if (challenges.SeasonChallengeHistory) {
addSeasonalChallengeHistory(inventory, challenges.SeasonChallengeHistory); challenges.SeasonChallengeHistory.forEach(({ challenge, id }) => {
} const itemIndex = inventory.SeasonChallengeHistory.findIndex(i => i.challenge === challenge);
const affiliationMods: IAffiliationMods[] = []; if (itemIndex !== -1) {
if (challenges.ChallengeProgress && challenges.SeasonChallengeCompletions) { inventory.SeasonChallengeHistory[itemIndex].id = id;
for (const challenge of challenges.SeasonChallengeCompletions) { } else {
// Ignore challenges that weren't completed just now inventory.SeasonChallengeHistory.push({ challenge, id });
if (!challenges.ChallengeProgress.find(x => challenge.challenge.indexOf(x.Name) != -1)) {
continue;
}
const meta = ExportNightwave.challenges[challenge.challenge];
logger.debug("Completed challenge", meta);
let affiliation = inventory.Affiliations.find(x => x.Tag == ExportNightwave.affiliationTag);
if (!affiliation) {
affiliation =
inventory.Affiliations[
inventory.Affiliations.push({
Tag: ExportNightwave.affiliationTag,
Standing: 0
}) - 1
];
}
affiliation.Standing += meta.standing;
if (affiliationMods.length == 0) {
affiliationMods.push({ Tag: ExportNightwave.affiliationTag });
}
affiliationMods[0].Standing ??= 0;
affiliationMods[0].Standing += meta.standing;
} }
});
} }
await inventory.save(); await inventory.save();

View File

@ -5,7 +5,6 @@ import { updateQuestKey, IUpdateQuestRequest } from "@/src/services/questService
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const updateQuestController: RequestHandler = async (req, res) => { export const updateQuestController: RequestHandler = async (req, res) => {
const accountId = parseString(req.query.accountId); const accountId = parseString(req.query.accountId);
const updateQuestRequest = getJSONfromString<IUpdateQuestRequest>((req.body as string).toString()); const updateQuestRequest = getJSONfromString<IUpdateQuestRequest>((req.body as string).toString());

View File

@ -11,7 +11,7 @@ import { getAccountIdForRequest } from "@/src/services/loginService";
import { addMiscItems, addRecipes, getInventory, updateCurrency } from "@/src/services/inventoryService"; import { addMiscItems, addRecipes, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { getRecipeByResult } from "@/src/services/itemDataService"; import { getRecipeByResult } from "@/src/services/itemDataService";
import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { addInfestedFoundryXP } from "./infestedFoundryController"; import { addInfestedFoundryXP, applyCheatsToInfestedFoundry } from "@/src/services/infestedFoundryService";
import { config } from "@/src/services/configService"; import { config } from "@/src/services/configService";
export const upgradesController: RequestHandler = async (req, res) => { export const upgradesController: RequestHandler = async (req, res) => {
@ -25,7 +25,13 @@ export const upgradesController: RequestHandler = async (req, res) => {
operation.UpgradeRequirement == "/Lotus/Types/Items/MiscItems/CustomizationSlotUnlocker" operation.UpgradeRequirement == "/Lotus/Types/Items/MiscItems/CustomizationSlotUnlocker"
) { ) {
updateCurrency(inventory, 10, true); updateCurrency(inventory, 10, true);
} else { } else if (
operation.OperationType != "UOT_SWAP_POLARITY" &&
operation.OperationType != "UOT_ABILITY_OVERRIDE"
) {
if (!operation.UpgradeRequirement) {
throw new Error(`${operation.OperationType} operation should be free?`);
}
addMiscItems(inventory, [ addMiscItems(inventory, [
{ {
ItemType: operation.UpgradeRequirement, ItemType: operation.UpgradeRequirement,
@ -66,6 +72,7 @@ export const upgradesController: RequestHandler = async (req, res) => {
inventoryChanges.Recipes = recipeChanges; inventoryChanges.Recipes = recipeChanges;
inventoryChanges.InfestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry; inventoryChanges.InfestedFoundry = inventory.toJSON<IInventoryClient>().InfestedFoundry;
applyCheatsToInfestedFoundry(inventoryChanges.InfestedFoundry!);
} else } else
switch (operation.UpgradeRequirement) { switch (operation.UpgradeRequirement) {
case "/Lotus/Types/Items/MiscItems/OrokinReactor": case "/Lotus/Types/Items/MiscItems/OrokinReactor":

View File

@ -1,6 +1,6 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { Account } from "@/src/models/loginModel"; import { Account, Ignore } from "@/src/models/loginModel";
import { Inbox } from "@/src/models/inboxModel"; import { Inbox } from "@/src/models/inboxModel";
import { Inventory } from "@/src/models/inventoryModels/inventoryModel"; import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
import { Loadout } from "@/src/models/inventoryModels/loadoutModel"; import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
@ -9,13 +9,22 @@ import { Ship } from "@/src/models/shipModel";
import { Stats } from "@/src/models/statsModel"; import { Stats } from "@/src/models/statsModel";
import { GuildMember } from "@/src/models/guildModel"; import { GuildMember } from "@/src/models/guildModel";
import { Leaderboard } from "@/src/models/leaderboardModel"; import { Leaderboard } from "@/src/models/leaderboardModel";
import { deleteGuild } from "@/src/services/guildService";
export const deleteAccountController: RequestHandler = async (req, res) => { export const deleteAccountController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
// TODO: Handle the account being the creator of a guild
// If account is the founding warlord of a guild, delete that guild as well.
const guildMember = await GuildMember.findOne({ accountId, rank: 0, status: 0 });
if (guildMember) {
await deleteGuild(guildMember.guildId);
}
await Promise.all([ await Promise.all([
Account.deleteOne({ _id: accountId }), Account.deleteOne({ _id: accountId }),
GuildMember.deleteMany({ accountId: accountId }), GuildMember.deleteMany({ accountId: accountId }),
Ignore.deleteMany({ ignorer: accountId }),
Ignore.deleteMany({ ignoree: accountId }),
Inbox.deleteMany({ ownerId: accountId }), Inbox.deleteMany({ ownerId: accountId }),
Inventory.deleteOne({ accountOwnerId: accountId }), Inventory.deleteOne({ accountOwnerId: accountId }),
Leaderboard.deleteMany({ ownerId: accountId }), Leaderboard.deleteMany({ ownerId: accountId }),

View File

@ -1,4 +1,4 @@
import { Guild, GuildMember } from "@/src/models/guildModel"; import { AllianceMember, Guild, GuildMember } from "@/src/models/guildModel";
import { getAccountForRequest, isAdministrator } from "@/src/services/loginService"; import { getAccountForRequest, isAdministrator } from "@/src/services/loginService";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
@ -12,9 +12,19 @@ export const getAccountInfoController: RequestHandler = async (req, res) => {
} }
const guildMember = await GuildMember.findOne({ accountId: account._id, status: 0 }, "guildId rank"); const guildMember = await GuildMember.findOne({ accountId: account._id, status: 0 }, "guildId rank");
if (guildMember) { if (guildMember) {
const guild = (await Guild.findById(guildMember.guildId, "Ranks"))!; const guild = (await Guild.findById(guildMember.guildId, "Ranks AllianceId"))!;
info.GuildId = guildMember.guildId.toString(); info.GuildId = guildMember.guildId.toString();
info.GuildPermissions = guild.Ranks[guildMember.rank].Permissions; info.GuildPermissions = guild.Ranks[guildMember.rank].Permissions;
info.GuildRank = guildMember.rank;
if (guild.AllianceId) {
//const alliance = (await Alliance.findById(guild.AllianceId))!;
const allianceMember = (await AllianceMember.findOne({
allianceId: guild.AllianceId,
guildId: guild._id
}))!;
info.AllianceId = guild.AllianceId.toString();
info.AlliancePermissions = allianceMember.Permissions;
}
} }
res.json(info); res.json(info);
}; };
@ -24,4 +34,7 @@ interface IAccountInfo {
IsAdministrator?: boolean; IsAdministrator?: boolean;
GuildId?: string; GuildId?: string;
GuildPermissions?: number; GuildPermissions?: number;
GuildRank?: number;
AllianceId?: string;
AlliancePermissions?: number;
} }

View File

@ -3,8 +3,10 @@ import { getDict, getItemName, getString } from "@/src/services/itemDataService"
import { import {
ExportArcanes, ExportArcanes,
ExportAvionics, ExportAvionics,
ExportCustoms,
ExportDrones, ExportDrones,
ExportGear, ExportGear,
ExportKeys,
ExportMisc, ExportMisc,
ExportRailjackWeapons, ExportRailjackWeapons,
ExportRecipes, ExportRecipes,
@ -25,6 +27,8 @@ interface ListedItem {
fusionLimit?: number; fusionLimit?: number;
exalted?: string[]; exalted?: string[];
badReason?: "starter" | "frivolous" | "notraw"; badReason?: "starter" | "frivolous" | "notraw";
partType?: string;
chainLength?: number;
} }
const relicQualitySuffixes: Record<TRelicQuality, string> = { const relicQualitySuffixes: Record<TRelicQuality, string> = {
@ -50,6 +54,9 @@ const getItemListsController: RequestHandler = (req, response) => {
res.MechSuits = []; res.MechSuits = [];
res.miscitems = []; res.miscitems = [];
res.Syndicates = []; res.Syndicates = [];
res.OperatorAmps = [];
res.QuestKeys = [];
res.KubrowPets = [];
for (const [uniqueName, item] of Object.entries(ExportWarframes)) { for (const [uniqueName, item] of Object.entries(ExportWarframes)) {
res[item.productCategory].push({ res[item.productCategory].push({
uniqueName, uniqueName,
@ -58,7 +65,7 @@ const getItemListsController: RequestHandler = (req, response) => {
}); });
} }
for (const [uniqueName, item] of Object.entries(ExportSentinels)) { for (const [uniqueName, item] of Object.entries(ExportSentinels)) {
if (item.productCategory == "Sentinels") { if (item.productCategory != "SpecialItems") {
res[item.productCategory].push({ res[item.productCategory].push({
uniqueName, uniqueName,
name: getString(item.name, lang) name: getString(item.name, lang)
@ -66,21 +73,14 @@ const getItemListsController: RequestHandler = (req, response) => {
} }
} }
for (const [uniqueName, item] of Object.entries(ExportWeapons)) { for (const [uniqueName, item] of Object.entries(ExportWeapons)) {
if ( if (item.partType) {
uniqueName.split("/")[4] == "OperatorAmplifiers" || if (!uniqueName.startsWith("/Lotus/Types/Items/Deimos/")) {
uniqueName.split("/")[5] == "SUModularSecondarySet1" ||
uniqueName.split("/")[5] == "SUModularPrimarySet1" ||
uniqueName.split("/")[5] == "InfKitGun" ||
uniqueName.split("/")[5] == "HoverboardParts" ||
uniqueName.split("/")[5] == "ModularMelee01" ||
uniqueName.split("/")[5] == "ModularMelee02" ||
uniqueName.split("/")[5] == "ModularMeleeInfested" ||
uniqueName.split("/")[6] == "CreaturePetParts"
) {
res.ModularParts.push({ res.ModularParts.push({
uniqueName, uniqueName,
name: getString(item.name, lang) name: getString(item.name, lang),
partType: item.partType
}); });
}
if (uniqueName.split("/")[5] != "SentTrainingAmplifier") { if (uniqueName.split("/")[5] != "SentTrainingAmplifier") {
res.miscitems.push({ res.miscitems.push({
uniqueName: uniqueName, uniqueName: uniqueName,
@ -94,7 +94,8 @@ const getItemListsController: RequestHandler = (req, response) => {
item.productCategory == "Melee" || item.productCategory == "Melee" ||
item.productCategory == "SpaceGuns" || item.productCategory == "SpaceGuns" ||
item.productCategory == "SpaceMelee" || item.productCategory == "SpaceMelee" ||
item.productCategory == "SentinelWeapons" item.productCategory == "SentinelWeapons" ||
item.productCategory == "OperatorAmps"
) { ) {
res[item.productCategory].push({ res[item.productCategory].push({
uniqueName, uniqueName,
@ -121,6 +122,7 @@ const getItemListsController: RequestHandler = (req, response) => {
} }
} }
if ( if (
name &&
uniqueName.substr(0, 30) != "/Lotus/Types/Game/Projections/" && uniqueName.substr(0, 30) != "/Lotus/Types/Game/Projections/" &&
uniqueName != "/Lotus/Types/Gameplay/EntratiLab/Resources/EntratiLanthornBundle" uniqueName != "/Lotus/Types/Gameplay/EntratiLab/Resources/EntratiLanthornBundle"
) { ) {
@ -152,9 +154,11 @@ const getItemListsController: RequestHandler = (req, response) => {
if (!item.hidden) { if (!item.hidden) {
const resultName = getItemName(item.resultType); const resultName = getItemName(item.resultType);
if (resultName) { if (resultName) {
let itemName = getString(resultName, lang);
if (item.num > 1) itemName = `${itemName} X ${item.num}`;
res.miscitems.push({ res.miscitems.push({
uniqueName: uniqueName, uniqueName: uniqueName,
name: recipeNameTemplate.replace("|ITEM|", getString(resultName, lang)) name: recipeNameTemplate.replace("|ITEM|", itemName)
}); });
} }
} }
@ -171,6 +175,12 @@ const getItemListsController: RequestHandler = (req, response) => {
name: getString(item.name, lang) name: getString(item.name, lang)
}); });
} }
for (const [uniqueName, item] of Object.entries(ExportCustoms)) {
res.miscitems.push({
uniqueName: uniqueName,
name: getString(item.name, lang)
});
}
res.mods = []; res.mods = [];
for (const [uniqueName, upgrade] of Object.entries(ExportUpgrades)) { for (const [uniqueName, upgrade] of Object.entries(ExportUpgrades)) {
@ -213,6 +223,20 @@ const getItemListsController: RequestHandler = (req, response) => {
name: getString(syndicate.name, lang) name: getString(syndicate.name, lang)
}); });
} }
for (const [uniqueName, key] of Object.entries(ExportKeys)) {
if (key.chainStages) {
res.QuestKeys.push({
uniqueName,
name: getString(key.name || "", lang),
chainLength: key.chainStages.length
});
} else if (key.name) {
res.miscitems.push({
uniqueName,
name: getString(key.name, lang)
});
}
}
response.json({ response.json({
archonCrystalUpgrades, archonCrystalUpgrades,

View File

@ -1,7 +1,11 @@
import { addString } from "@/src/controllers/api/inventoryController";
import { getInventory } from "@/src/services/inventoryService"; import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { addQuestKey, completeQuest, IUpdateQuestRequest, updateQuestKey } from "@/src/services/questService"; import {
addQuestKey,
completeQuest,
giveKeyChainMissionReward,
giveKeyChainStageTriggered
} from "@/src/services/questService";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { ExportKeys } from "warframe-public-export-plus"; import { ExportKeys } from "warframe-public-export-plus";
@ -9,13 +13,17 @@ import { ExportKeys } from "warframe-public-export-plus";
export const manageQuestsController: RequestHandler = async (req, res) => { export const manageQuestsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const operation = req.query.operation as const operation = req.query.operation as
| "unlockAll"
| "completeAll" | "completeAll"
| "ResetAll" | "resetAll"
| "completeAllUnlocked" | "giveAll"
| "updateKey" | "completeKey"
| "giveAll"; | "deleteKey"
const questKeyUpdate = req.body as IUpdateQuestRequest["QuestKeys"]; | "resetKey"
| "prevStage"
| "nextStage"
| "setInactive";
const questItemType = req.query.itemType as string;
const allQuestKeys: string[] = []; const allQuestKeys: string[] = [];
for (const [k, v] of Object.entries(ExportKeys)) { for (const [k, v] of Object.entries(ExportKeys)) {
@ -26,47 +34,13 @@ export const manageQuestsController: RequestHandler = async (req, res) => {
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
switch (operation) { switch (operation) {
case "updateKey": {
//TODO: if this is intended to be used, one needs to add a updateQuestKeyMultiple, the game does never intend to do it, so it errors for multiple keys.
await updateQuestKey(inventory, questKeyUpdate);
break;
}
case "unlockAll": {
for (const questKey of allQuestKeys) {
addQuestKey(inventory, { ItemType: questKey, Completed: false, unlock: true, Progress: [] });
}
break;
}
case "completeAll": { case "completeAll": {
logger.info("completing all quests.."); for (const questKey of inventory.QuestKeys) {
for (const questKey of allQuestKeys) { await completeQuest(inventory, questKey.ItemType);
try {
await completeQuest(inventory, questKey);
} catch (error) {
if (error instanceof Error) {
logger.error(
`Something went wrong completing quest ${questKey}, probably could not add some item`
);
logger.error(error.message);
}
}
//Skip "Watch The Maker"
if (questKey === "/Lotus/Types/Keys/NewWarIntroQuest/NewWarIntroKeyChain") {
addString(
inventory.NodeIntrosCompleted,
"/Lotus/Levels/Cinematics/NewWarIntro/NewWarStageTwo.level"
);
}
if (questKey === "/Lotus/Types/Keys/ArchwingQuest/ArchwingQuestKeyChain") {
inventory.ArchwingEnabled = true;
}
} }
break; break;
} }
case "ResetAll": { case "resetAll": {
logger.info("resetting all quests..");
for (const questKey of inventory.QuestKeys) { for (const questKey of inventory.QuestKeys) {
questKey.Completed = false; questKey.Completed = false;
questKey.Progress = []; questKey.Progress = [];
@ -75,40 +49,110 @@ export const manageQuestsController: RequestHandler = async (req, res) => {
inventory.ActiveQuest = ""; inventory.ActiveQuest = "";
break; break;
} }
case "completeAllUnlocked": {
logger.info("completing all unlocked quests..");
for (const questKey of inventory.QuestKeys) {
try {
await completeQuest(inventory, questKey.ItemType);
} catch (error) {
if (error instanceof Error) {
logger.error(
`Something went wrong completing quest ${questKey.ItemType}, probably could not add some item`
);
logger.error(error.message);
}
}
//Skip "Watch The Maker"
if (questKey.ItemType === "/Lotus/Types/Keys/NewWarIntroQuest/NewWarIntroKeyChain") {
addString(
inventory.NodeIntrosCompleted,
"/Lotus/Levels/Cinematics/NewWarIntro/NewWarStageTwo.level"
);
}
if (questKey.ItemType === "/Lotus/Types/Keys/ArchwingQuest/ArchwingQuestKeyChain") {
inventory.ArchwingEnabled = true;
}
}
break;
}
case "giveAll": { case "giveAll": {
for (const questKey of allQuestKeys) { allQuestKeys.forEach(questKey => addQuestKey(inventory, { ItemType: questKey }));
addQuestKey(inventory, { ItemType: questKey }); break;
}
case "deleteKey": {
const questKey = inventory.QuestKeys.find(key => key.ItemType === questItemType);
if (!questKey) {
logger.error(`Quest key not found in inventory: ${questItemType}`);
break;
}
inventory.QuestKeys.pull({ ItemType: questItemType });
break;
}
case "completeKey": {
if (allQuestKeys.includes(questItemType)) {
const questKey = inventory.QuestKeys.find(key => key.ItemType === questItemType);
if (!questKey) {
logger.error(`Quest key not found in inventory: ${questItemType}`);
break;
}
await completeQuest(inventory, questItemType);
} }
break; break;
} }
case "resetKey": {
if (allQuestKeys.includes(questItemType)) {
const questKey = inventory.QuestKeys.find(key => key.ItemType === questItemType);
if (!questKey) {
logger.error(`Quest key not found in inventory: ${questItemType}`);
break;
}
questKey.Completed = false;
questKey.Progress = [];
questKey.CompletionDate = undefined;
}
break;
}
case "prevStage": {
if (allQuestKeys.includes(questItemType)) {
const questKey = inventory.QuestKeys.find(key => key.ItemType === questItemType);
if (!questKey) {
logger.error(`Quest key not found in inventory: ${questItemType}`);
break;
}
if (!questKey.Progress) break;
if (questKey.Completed) {
questKey.Completed = false;
questKey.CompletionDate = undefined;
}
questKey.Progress.pop();
const stage = questKey.Progress.length - 1;
if (stage > 0) {
await giveKeyChainStageTriggered(inventory, {
KeyChain: questKey.ItemType,
ChainStage: stage
});
}
}
break;
}
case "nextStage": {
if (allQuestKeys.includes(questItemType)) {
const questKey = inventory.QuestKeys.find(key => key.ItemType === questItemType);
const questManifest = ExportKeys[questItemType];
if (!questKey) {
logger.error(`Quest key not found in inventory: ${questItemType}`);
break;
}
if (!questKey.Progress) break;
const currentStage = questKey.Progress.length;
if (currentStage + 1 == questManifest.chainStages?.length) {
logger.debug(`Trying to complete last stage with nextStage, calling completeQuest instead`);
await completeQuest(inventory, questKey.ItemType);
} else {
const progress = {
c: questManifest.chainStages![currentStage].key ? -1 : 0,
i: false,
m: false,
b: []
};
questKey.Progress.push(progress);
await giveKeyChainStageTriggered(inventory, {
KeyChain: questKey.ItemType,
ChainStage: currentStage
});
if (currentStage > 0) {
await giveKeyChainMissionReward(inventory, {
KeyChain: questKey.ItemType,
ChainStage: currentStage - 1
});
}
}
}
break;
}
case "setInactive":
inventory.ActiveQuest = "";
break;
} }
await inventory.save(); await inventory.save();

View File

@ -5,7 +5,7 @@ import { getInventory } from "@/src/services/inventoryService";
export const popArchonCrystalUpgradeController: RequestHandler = async (req, res) => { export const popArchonCrystalUpgradeController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const suit = inventory.Suits.find(suit => suit._id.toString() == (req.query.oid as string)); const suit = inventory.Suits.id(req.query.oid as string);
if (suit && suit.ArchonCrystalUpgrades) { if (suit && suit.ArchonCrystalUpgrades) {
suit.ArchonCrystalUpgrades = suit.ArchonCrystalUpgrades.filter( suit.ArchonCrystalUpgrades = suit.ArchonCrystalUpgrades.filter(
x => x.UpgradeType != (req.query.type as string) x => x.UpgradeType != (req.query.type as string)

View File

@ -5,7 +5,7 @@ import { getInventory } from "@/src/services/inventoryService";
export const pushArchonCrystalUpgradeController: RequestHandler = async (req, res) => { export const pushArchonCrystalUpgradeController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId); const inventory = await getInventory(accountId);
const suit = inventory.Suits.find(suit => suit._id.toString() == (req.query.oid as string)); const suit = inventory.Suits.id(req.query.oid as string);
if (suit) { if (suit) {
suit.ArchonCrystalUpgrades ??= []; suit.ArchonCrystalUpgrades ??= [];
const count = (req.query.count as number | undefined) ?? 1; const count = (req.query.count as number | undefined) ?? 1;

View File

@ -1,6 +1,7 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { getAccountForRequest, isAdministrator, isNameTaken } from "@/src/services/loginService"; import { getAccountForRequest, isAdministrator, isNameTaken } from "@/src/services/loginService";
import { config, saveConfig } from "@/src/services/configService"; import { config } from "@/src/services/configService";
import { saveConfig } from "@/src/services/configWatcherService";
export const renameAccountController: RequestHandler = async (req, res) => { export const renameAccountController: RequestHandler = async (req, res) => {
const account = await getAccountForRequest(req); const account = await getAccountForRequest(req);

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