Compare commits

...

270 Commits

Author SHA1 Message Date
9468768947 fix: weapon seed's low dword being sign extended (#1914)
JavaScript's semantics here are incredibly stupid, but basically if the initial DWORD's high WORD's MSB is true, the number would become negative after the shift left by 16. Then when ORing it with the highDword, the initial DWORD would be sign-extended to a QWORD, meaning the high DWORD would become all 1s, basically cancelling out the entire OR operation.

Reviewed-on: OpenWF/SpaceNinjaServer#1914
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-29 12:28:01 -07:00
0af7f41201 fix: unset LibraryPersonalTarget after completing it (#1913)
Reviewed-on: OpenWF/SpaceNinjaServer#1913
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-29 12:27:47 -07:00
de1e2a25f2 fix(webui): ensure that all requests using authz revalidate it (#1911)
Closes #1907

Reviewed-on: OpenWF/SpaceNinjaServer#1911
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-29 12:27:38 -07:00
1cf7b41d3f chore: note that random element functions could return undefined (#1910)
We should be explicit about the fact that we expect the arrays to not be empty.

Reviewed-on: OpenWF/SpaceNinjaServer#1910
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-29 12:27:25 -07:00
ab9cc685eb fix: exclude capture as a mission type for sorties (#1909)
Closes #1865

Reviewed-on: OpenWF/SpaceNinjaServer#1909
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-29 02:05:18 -07:00
743b784754 chore(webui): use plural form of "Moa", just to stay consistent with the other categories (#1905)
Reviewed-on: OpenWF/SpaceNinjaServer#1905
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-04-28 14:01:35 -07:00
5df533a7fb chore: auto-generate "daily special" for fish vendors (#1902)
Trying to go a bit more towards an "auto-generate by default" approach, with manual overrides where needed.

Reviewed-on: OpenWF/SpaceNinjaServer#1902
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-28 14:01:17 -07:00
9417aa3c84 fix: only consider market-listed blueprints for login reward (#1900)
Closes #1882

Reviewed-on: OpenWF/SpaceNinjaServer#1900
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-28 14:01:02 -07:00
a1872e2b07 chore: simplify getInnateDamageTag (#1899)
Reviewed-on: OpenWF/SpaceNinjaServer#1899
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-28 14:00:51 -07:00
9042e85355 feat: infested lich rewards (#1898)
Closes #1884

Reviewed-on: OpenWF/SpaceNinjaServer#1898
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-28 14:00:38 -07:00
66ee550ccd feat: refresh duviri seed when mood changes (#1895)
Closes #1887

Reviewed-on: OpenWF/SpaceNinjaServer#1895
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-28 14:00:22 -07:00
7a295a86ec fix: handle boosters in store item utilities (#1894)
e.g. `/Lotus/Types/StoreItems/Boosters/AffinityBoosterStoreItem`

Reviewed-on: OpenWF/SpaceNinjaServer#1894
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-28 14:00:06 -07:00
88d00eaaa1 feat: weaken nemesis (#1893)
Closes #1885

Reviewed-on: OpenWF/SpaceNinjaServer#1893
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-28 13:59:54 -07:00
1e8f2fc766 chore: comment out mixed fields in inventory (#1892)
If they are needed in the future, they schould be properly schema'd.

Reviewed-on: OpenWF/SpaceNinjaServer#1892
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-28 13:58:39 -07:00
0d842ade90 chore(webui): update German translation (#1904)
Reviewed-on: OpenWF/SpaceNinjaServer#1904
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-04-28 05:14:25 -07:00
4e3a2e17ee chore: removing unnecessary entries in allScans.json (#1903)
Reviewed-on: OpenWF/SpaceNinjaServer#1903
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-04-28 03:36:39 -07:00
61864b2be1 chore(webui): update to Spanish translation (#1901)
Reviewed-on: OpenWF/SpaceNinjaServer#1901
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-04-27 19:39:49 -07:00
45748fa8be fix: import failing for LotusCustomization from live (#1891)
Reviewed-on: OpenWF/SpaceNinjaServer#1891
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-27 14:20:52 -07:00
afec59e8a6 feat: skipClanKeyCrafting cheat (#1883)
Closes #1843

Reviewed-on: OpenWF/SpaceNinjaServer#1883
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-27 12:38:55 -07:00
ee1a49f5f2 feat: handle NemesisKillConvert at missionInventoryUpdate (#1880)
Closes #1848

Reviewed-on: OpenWF/SpaceNinjaServer#1880
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-27 12:38:48 -07:00
9e94083875 feat: handle KubrowPetEggs in missionInventoryUpdate (#1876)
Closes #1866

Reviewed-on: OpenWF/SpaceNinjaServer#1876
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-27 12:36:00 -07:00
db0e0d80dd chore: remove PropertyTextHash from auto-generated vendors 2025-04-27 07:20:04 +02:00
5cda2e2d08 chore: improve unlockAllScans's handling of existing scans (#1875)
Reviewed-on: OpenWF/SpaceNinjaServer#1875
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-26 19:28:03 -07:00
e23d865044 fix: use a list of "known good" syndicate missions (#1874)
Closes #1870

Reviewed-on: OpenWF/SpaceNinjaServer#1874
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-26 19:27:50 -07:00
c7658b5b20 chore: use parallelForeach in removePigmentsFromGuildMembers 2025-04-27 04:22:18 +02:00
9993500eca chore(webui): update to Spanish translation (#1881)
Reviewed-on: OpenWF/SpaceNinjaServer#1881
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-04-26 18:53:46 -07:00
267357871b feat: handle HenchmenKilled & HintProgress incrementing (#1877)
Closes #1807

Reviewed-on: OpenWF/SpaceNinjaServer#1877
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-26 15:25:47 -07:00
cf5ed0442d fix: don't assume rewardInfo.node is in ExportRegions (#1879)
Fixes #1878

Reviewed-on: OpenWF/SpaceNinjaServer#1879
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-26 14:23:00 -07:00
de36e2ee8d fix: close connection for dating saveDialogue request (#1873)
Missing fix for #1852

Reviewed-on: OpenWF/SpaceNinjaServer#1873
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-26 12:14:42 -07:00
ca1b6c31b6 fix: give rewards for completing a capture mission (#1872)
Reviewed-on: OpenWF/SpaceNinjaServer#1872
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-26 11:57:03 -07:00
d66c474bfc fix: some issues with sortie generation (#1871)
Now using sortieTilesets as a source of truth for allowed mission nodes as it's based only on real sorties, also added disallowed mission types for FC_OROKIN (Corrupted Vor) that otherwise cause a script error.

Closes #1865

Reviewed-on: OpenWF/SpaceNinjaServer#1871
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-26 11:56:41 -07:00
781f01520f feat: save lotus customization (#1864)
Closes #768

Reviewed-on: OpenWF/SpaceNinjaServer#1864
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-26 11:56:22 -07:00
ac37702468 feat(webui): add missing max rank mods (#1863)
Closes #916

Reviewed-on: OpenWF/SpaceNinjaServer#1863
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-26 11:56:16 -07:00
75c011e3cb fix: don't set IsNew flag for starting gear (#1859)
Reviewed-on: OpenWF/SpaceNinjaServer#1859
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-26 11:56:06 -07:00
4d4f885c8e feat: dontSubtractConsumables cheat (#1857)
Closes #1838

Reviewed-on: OpenWF/SpaceNinjaServer#1857
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-26 11:55:45 -07:00
66d1a65e63 fix: handle credits & platinum prices from vendors (#1856)
Fixes #1837

Reviewed-on: OpenWF/SpaceNinjaServer#1856
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-26 11:55:03 -07:00
48eefd8db1 fix: don't give droptable rewards for non-assassination sortie missions (#1855)
Closes #1835

Reviewed-on: OpenWF/SpaceNinjaServer#1855
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-26 11:54:54 -07:00
4a6a5ea9cc feat: handle WeaponSkins picked up in missions (#1854)
For sigils.

Closes #1839

Reviewed-on: OpenWF/SpaceNinjaServer#1854
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-26 11:54:38 -07:00
95c0ad7892 fix: handle saveDialogue request without Data or Gift (#1853)
Needed to just set booleans when starting dating.

Closes #1852

Reviewed-on: OpenWF/SpaceNinjaServer#1853
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-26 11:54:25 -07:00
a90d3a5156 feat: gardening (#1849)
Reviewed-on: OpenWF/SpaceNinjaServer#1849
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-26 11:54:06 -07:00
d0c9409a2d fix: exclude pvp variants from daily special parts (#1846)
Fixes #1836

Reviewed-on: OpenWF/SpaceNinjaServer#1846
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-26 11:53:41 -07:00
bbde7b2141 chore: don't change remote/origin url 2025-04-26 08:12:54 +02:00
5271123090 chore(webui): update to Spanish translation (#1862)
Reviewed-on: OpenWF/SpaceNinjaServer#1862
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-04-25 22:13:28 -07:00
f3e56480e5 fix(webui): error for inventory without EvolutionProgress (#1861)
Reviewed-on: OpenWF/SpaceNinjaServer#1861
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-25 21:26:58 -07:00
6f46ace40c fix(webui): revalidate authz for rename & delete account actions (#1860)
Reviewed-on: OpenWF/SpaceNinjaServer#1860
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-25 21:26:52 -07:00
883426e429 fix: align guild advertisment vendor rotation to monday 0 UTC (#1858)
Reviewed-on: OpenWF/SpaceNinjaServer#1858
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-25 21:26:16 -07:00
13432bf034 fix: future-proof oid string generation (#1847)
This ensures they are still 24 bytes long even past the year 2106. :^)

Reviewed-on: OpenWF/SpaceNinjaServer#1847
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-25 18:55:09 -07:00
a1267e5f64 chore: add temple vendor manifest (#1851)
Closes #1850

Reviewed-on: OpenWF/SpaceNinjaServer#1851
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-25 18:54:52 -07:00
2058207b6a fix: nemesis fp from client could be of type number 2025-04-26 03:15:58 +02:00
c7c416c100 chore: simplify arguments defaulted to undefined 2025-04-26 00:40:57 +02:00
90e97d7888 chore: auto-generate guild advertisment vendor (#1845)
With this, preprocessing is simplified to just refreshing expiry dates. No real change to auto-generation logic.

Reviewed-on: OpenWF/SpaceNinjaServer#1845
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-25 15:12:45 -07:00
3f6734ac1c feat(webui): EvolutionProgress support (#1818)
Closes #1815

Reviewed-on: OpenWF/SpaceNinjaServer#1818
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-04-25 12:00:38 -07:00
6f64690b91 fix: refresh duviri seed after non-quit completion of a duviri game mode (#1834)
Reviewed-on: OpenWF/SpaceNinjaServer#1834
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-25 11:56:40 -07:00
fb5a7320bb chore: update to allScans cheat (#1844)
Reviewed-on: OpenWF/SpaceNinjaServer#1844
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-04-25 11:56:27 -07:00
143b358a03 chore: always update rewardSeed in missionInventoryUpdate (#1833)
This should be slightly more faithful. Also logging a warning in case we have a mismatch as that shouldn't happen.

Reviewed-on: OpenWF/SpaceNinjaServer#1833
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-25 11:54:11 -07:00
0b75757277 chore: improve distribution of rewardSeed (#1831)
This was previously not ideal due to float imprecision, but now it's 64 bits and there's enough entropy for all of them.

Reviewed-on: OpenWF/SpaceNinjaServer#1831
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-25 11:53:54 -07:00
fd7f4c9e92 feat: calendar progress (#1830)
Closes #1775

Reviewed-on: OpenWF/SpaceNinjaServer#1830
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-25 11:53:34 -07:00
fa6fac494b fix: some problems with 1999 calendar rotation (#1829)
- First day was incorrect for summer & autumn
- Only 1 reward was shown, now is a choice of 2
- Only 1 upgrade was shown, now is a choice of 3
- First 2 challenges in the season are now guaranteed to be "easy"

Reviewed-on: OpenWF/SpaceNinjaServer#1829
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-25 11:53:04 -07:00
6b3f524574 feat: sortie mission credit rewards (#1828)
Closes #1820

Reviewed-on: OpenWF/SpaceNinjaServer#1828
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-25 11:52:42 -07:00
506365f97e feat: auto-generate debt token vendor manifest (#1827)
Yet another pretty big change to how these things are generated, but getting closer to where we wanna be now.

Reviewed-on: OpenWF/SpaceNinjaServer#1827
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-25 11:52:31 -07:00
70646160c3 fix: give no rewards if there are no qualifications (#1826)
Fixes #1823

Reviewed-on: OpenWF/SpaceNinjaServer#1826
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-25 11:52:16 -07:00
3ffa4a7fd3 fix: exclude some more nodes from syndicate missions (#1825)
Closes #1819

Reviewed-on: OpenWF/SpaceNinjaServer#1825
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-25 11:51:54 -07:00
826a09a473 fix: provide a response to setShipFavouriteLoadout (#1824)
Seems to be the same format as the request, so just mirror it back. This is so the client knows we acknowledged the change as it won't resync the ship until the next login.

Closes #1822

Reviewed-on: OpenWF/SpaceNinjaServer#1824
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-25 11:51:43 -07:00
100aefcee4 fix: give corresponding weapon when crafting Hound (#1816)
Reviewed-on: OpenWF/SpaceNinjaServer#1816
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-24 11:24:53 -07:00
409c089d11 feat: handle account already owning a nightwave skin item (#1814)
Closes #1811

Reviewed-on: OpenWF/SpaceNinjaServer#1814
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-24 11:24:38 -07:00
8c32dc2670 fix: add MoaPets into sellController (#1813)
Reviewed-on: OpenWF/SpaceNinjaServer#1813
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-24 11:24:25 -07:00
a67f99b665 chore: don't use sequential values as RNG seeds directly (#1812)
This should help get a slightly better distribution

Reviewed-on: OpenWF/SpaceNinjaServer#1812
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-24 11:24:11 -07:00
756a01d270 fix: pass Emblem field on in getGuildClient 2025-04-24 05:36:16 +02:00
efc7467a99 chore: remove unused MoaPets array from getItemLists 2025-04-24 02:00:27 +02:00
99e1a66da8 chore: improve typings in getItemLists 2025-04-24 00:46:33 +02:00
370f8c1008 fix: getItemLists fixup 2025-04-24 00:42:33 +02:00
f039998d71 chore: update PE+ to 0.5.58 2025-04-24 00:33:20 +02:00
eb594af9d8 chore: improve archwing mission detection (#1794)
SettlementNode10 was not being excluded

Reviewed-on: OpenWF/SpaceNinjaServer#1794
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-23 11:40:45 -07:00
bb8596fa87 fix(webui): use proper 'size' abbreviations for vallis & deimos fish (#1804)
Closes #1763

Reviewed-on: OpenWF/SpaceNinjaServer#1804
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-23 11:38:04 -07:00
a85539a686 feat: set IsNew flag on new sentinels (#1802)
Reviewed-on: OpenWF/SpaceNinjaServer#1802
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-23 11:37:52 -07:00
ada6a4bad0 fix: occupy correct slot for arch-guns (#1801)
Reviewed-on: OpenWF/SpaceNinjaServer#1801
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-23 11:37:43 -07:00
948104a9a6 fix: "logged in elsewhere" when logging in on account created via webui (#1800)
Reviewed-on: OpenWF/SpaceNinjaServer#1800
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-23 11:37:31 -07:00
7a8b12b372 chore: cap FusionPoints balance at 2147483647 (#1797)
Same idea as with typeCountSchema. The game needs to be able to store these safely in an i32 on the C++ side.

Reviewed-on: OpenWF/SpaceNinjaServer#1797
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-23 11:37:10 -07:00
26d644a982 feat: handle scale for the dojo decos that need it (#1795)
Closes #1785

Reviewed-on: OpenWF/SpaceNinjaServer#1795
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-23 11:36:57 -07:00
d6750cd84b chore: provide tileset for sortie missions (#1793)
Closes #1788

Reviewed-on: OpenWF/SpaceNinjaServer#1793
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-23 11:36:32 -07:00
f3601ec43e chore: allow MT_CAPTURE for sorties (#1792)
e.g. `SolNode1` is a capture mission but it should still be a valid node for sorties. Not that the mission will actually be a capture.

Reviewed-on: OpenWF/SpaceNinjaServer#1792
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-23 11:36:19 -07:00
15aaa28a4f feat: conquest progression & rewards (#1791)
Closes #1570

Co-authored-by: Jānis <janisslsm@noreply.localhost>
Reviewed-on: OpenWF/SpaceNinjaServer#1791
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-23 11:35:57 -07:00
ce5b0fc9e2 fix: limit MT_LANDSCAPE sortie missions to PoE (#1790)
Closes #1789

Reviewed-on: OpenWF/SpaceNinjaServer#1790
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-23 11:35:29 -07:00
64290b72c0 chore(webui): update to Spanish translation (#1809)
Reviewed-on: OpenWF/SpaceNinjaServer#1809
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-04-23 11:20:40 -07:00
570c6fe0d1 chore(webui): update German translation (#1806)
Reviewed-on: OpenWF/SpaceNinjaServer#1806
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-04-23 11:20:25 -07:00
146dbd1b89 chore(webui): update to Spanish translation (#1803)
Reviewed-on: OpenWF/SpaceNinjaServer#1803
Co-authored-by: hxedcl <hxedcl@noreply.localhost>
Co-committed-by: hxedcl <hxedcl@noreply.localhost>
2025-04-22 18:52:46 -07:00
e17d43dcb6 chore: fix slotNames duplication (#1798)
Reviewed-on: OpenWF/SpaceNinjaServer#1798
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-22 15:20:50 -07:00
daacbf6f7b fix(webui): add exalted array for KubrowPets ItemLists (#1782)
Closes #1770

Reviewed-on: OpenWF/SpaceNinjaServer#1782
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-04-22 10:00:58 -07:00
32bb6d4ccb feat: syndicate mission rotation (#1781)
Closes #1530

Reviewed-on: OpenWF/SpaceNinjaServer#1781
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-22 10:00:49 -07:00
23dafb53d1 fix: skipTutorial sets ReceivedStartingGear before giving the gear (#1780)
This was raising a warning when creating a new account.

Reviewed-on: OpenWF/SpaceNinjaServer#1780
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-22 10:00:38 -07:00
3aa853f953 feat(webui): register (#1779)
Closes #740

Reviewed-on: OpenWF/SpaceNinjaServer#1779
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-22 10:00:26 -07:00
409f41d3bf feat(webui): remove unranked mods (#1778)
Reviewed-on: OpenWF/SpaceNinjaServer#1778
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-22 10:00:10 -07:00
c4b8a71c5a chore(webui): provide "max rank" option when only exalted needs it (#1776)
Reviewed-on: OpenWF/SpaceNinjaServer#1776
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-22 10:00:04 -07:00
3b20a109f6 fix: handle saveDialogue without YearIteration having been supplied (#1774)
This is needed for The Hex rank up dialogues, which are independent of the year iterations.

Fixes #1773

Reviewed-on: OpenWF/SpaceNinjaServer#1774
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-22 09:59:54 -07:00
6d93ae9f2d fix: be less strict with required avatar type for personal synthesis (#1768)
Fixes #1766

Reviewed-on: OpenWF/SpaceNinjaServer#1768
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-22 09:59:41 -07:00
ad2f143f15 feat: cleanup some problems in inventories at daily reset (#1767)
Reviewed-on: OpenWF/SpaceNinjaServer#1767
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
2025-04-22 09:59:30 -07:00
03590c7360 chore(webui): update German translation (#1786)
Reviewed-on: OpenWF/SpaceNinjaServer#1786
Co-authored-by: Animan8000 <animan8000@noreply.localhost>
Co-committed-by: Animan8000 <animan8000@noreply.localhost>
2025-04-22 09:44:47 -07:00
e3fca682d6 Update static/webui/translations/fr.js (#1784)
Reviewed-on: OpenWF/SpaceNinjaServer#1784
Co-authored-by: Vitruvio <vitruvio@noreply.localhost>
Co-committed-by: Vitruvio <vitruvio@noreply.localhost>
2025-04-22 09:44:07 -07:00
c94bc3ef90 chore(webui): update Russian translation (#1783)
Reviewed-on: OpenWF/SpaceNinjaServer#1783
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-04-22 00:00:52 -07:00
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
140 changed files with 8906 additions and 3826 deletions

2
.gitattributes vendored
View File

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

View File

@ -5,17 +5,22 @@ on:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
version: [18, 20, 22]
steps:
- name: Checkout
uses: actions/checkout@v4.1.2
- name: Setup Node.js environment
uses: actions/setup-node@v4.0.2
with:
node-version: ${{ matrix.version }}
- run: npm ci
- run: cp config.json.example config.json
- 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,5 +1,3 @@
{
"recommendations": [
"dbaeumer.vscode-eslint"
]
}
"recommendations": ["dbaeumer.vscode-eslint"]
}

View File

@ -10,6 +10,8 @@ To get an idea of what functionality you can expect to be missing [have a look t
## config.json
SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [config.json.example](config.json.example), which has most cheats disabled.
- `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 ]`.
- `worldState.lockTime` will lock the time provided in worldState if nonzero, e.g. `1743202800` for night in POE.

View File

@ -1,7 +1,6 @@
@echo off
echo Updating SpaceNinjaServer...
git config remote.origin.url https://openwf.io/SpaceNinjaServer.git
git fetch --prune
git stash
git reset --hard origin/main

View File

@ -19,6 +19,7 @@
"infiniteEndo": false,
"infiniteRegalAya": false,
"infiniteHelminthMaterials": false,
"dontSubtractConsumables": false,
"unlockAllShipFeatures": false,
"unlockAllShipDecorations": false,
"unlockAllFlavourItems": false,
@ -29,11 +30,17 @@
"unlockExilusEverywhere": false,
"unlockArcanesEverywhere": false,
"noDailyStandingLimits": false,
"noDailyFocusLimit": false,
"noArgonCrystalDecay": false,
"noMasteryRankUpCooldown": false,
"noVendorPurchaseLimits": true,
"noDeathMarks": false,
"noKimCooldowns": false,
"instantResourceExtractorDrones": false,
"noResourceExtractorDronesDamage": false,
"skipClanKeyCrafting": false,
"noDojoRoomBuildStage": false,
"noDecoBuildStage": false,
"fastDojoRoomDestruction": false,
"noDojoResearchCosts": false,
"noDojoResearchTime": false,

16
package-lock.json generated
View File

@ -17,8 +17,8 @@
"mongoose": "^8.11.0",
"morgan": "^1.10.0",
"ncp": "^2.0.0",
"typescript": ">=5.5 <5.6.0",
"warframe-public-export-plus": "^0.5.50",
"typescript": "^5.5",
"warframe-public-export-plus": "^0.5.59",
"warframe-riven-info": "^0.1.2",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"
@ -3720,9 +3720,9 @@
}
},
"node_modules/typescript": {
"version": "5.5.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@ -3789,9 +3789,9 @@
}
},
"node_modules/warframe-public-export-plus": {
"version": "0.5.50",
"resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.50.tgz",
"integrity": "sha512-KlhdY/Q5sRAIn/RhmdviKBoX3gk+Jtuen0cWnFB2zqK7eKYMDtd79bKOtTPtnK9zCNzh6gFug2wEeDVam3Bwlw=="
"version": "0.5.59",
"resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.59.tgz",
"integrity": "sha512-/SUCVjngVDBz6gahz7CdVLywtHLODL6O5nmNtQcxFDUwrUGnF1lETcG8/UO+WLeGxBVAy4BDPbq+9ZWlYZM4uQ=="
},
"node_modules/warframe-riven-info": {
"version": "0.1.2",

View File

@ -4,11 +4,12 @@
"description": "WF Emulator",
"main": "index.ts",
"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 ",
"build": "tsc --incremental && ncp static/webui build/static/webui",
"build": "tsc --incremental --sourceMap && ncp static/webui build/static/webui",
"verify": "tsgo --noEmit",
"lint": "eslint --ext .ts .",
"lint:ci": "eslint --ext .ts --rule \"prettier/prettier: off\" .",
"lint:fix": "eslint --fix --ext .ts .",
"prettier": "prettier --write .",
"update-translations": "cd scripts && node update-translations.js"
@ -23,8 +24,8 @@
"mongoose": "^8.11.0",
"morgan": "^1.10.0",
"ncp": "^2.0.0",
"typescript": ">=5.5 <5.6.0",
"warframe-public-export-plus": "^0.5.50",
"typescript": "^5.5",
"warframe-public-export-plus": "^0.5.59",
"warframe-riven-info": "^0.1.2",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"

View File

@ -4,7 +4,7 @@
const fs = require("fs");
function extractStrings(content) {
const regex = /([a-zA-Z_]+): `([^`]*)`,/g;
const regex = /([a-zA-Z0-9_]+): `([^`]*)`,/g;
let matches;
const strings = {};
while ((matches = regex.exec(content)) !== null) {
@ -15,7 +15,7 @@ function extractStrings(content) {
const source = fs.readFileSync("../static/webui/translations/en.js", "utf8");
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 => {
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`);
}
});
} else if (line.length) {
} else {
fs.writeSync(fileHandle, line + "\n");
}
});

View File

@ -16,9 +16,9 @@ import { webuiRouter } from "@/src/routes/webui";
const app = express();
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.
if (req.headers["content-encoding"] == "ezip") {
if (req.headers["content-encoding"] == "ezip" || req.headers["content-encoding"] == "e") {
req.headers["content-encoding"] = undefined;
}
next();
@ -26,7 +26,7 @@ app.use((req, _res, next) => {
app.use(bodyParser.raw());
app.use(express.json({ limit: "4mb" }));
app.use(bodyParser.text());
app.use(bodyParser.text({ limit: "4mb" }));
app.use(requestLogger);
app.use("/api", apiRouter);

View File

@ -2,15 +2,18 @@ const millisecondsPerSecond = 1000;
const secondsPerMinute = 60;
const minutesPerHour = 60;
const hoursPerDay = 24;
const daysPerWeek = 7;
const unixSecond = millisecondsPerSecond;
const unixMinute = secondsPerMinute * millisecondsPerSecond;
const unixHour = unixMinute * minutesPerHour;
const unixDay = hoursPerDay * unixHour;
const unixWeek = daysPerWeek * unixDay;
export const unixTimesInMs = {
second: unixSecond,
minute: unixMinute,
hour: unixHour,
day: unixDay
day: unixDay,
week: unixWeek
};

View File

@ -17,7 +17,7 @@ export const activateRandomModController: RequestHandler = async (req, res) => {
ItemCount: -1
}
]);
const rivenType = getRandomElement(rivenRawToRealWeighted[request.ItemType]);
const rivenType = getRandomElement(rivenRawToRealWeighted[request.ItemType])!;
const fingerprint = createVeiledRivenFingerprint(ExportUpgrades[rivenType]);
const upgradeIndex =
inventory.Upgrades.push({

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

@ -28,7 +28,7 @@ export const artifactTransmutationController: RequestHandler = async (req, res)
});
const rawRivenType = getRandomRawRivenType();
const rivenType = getRandomElement(rivenRawToRealWeighted[rawRivenType]);
const rivenType = getRandomElement(rivenRawToRealWeighted[rawRivenType])!;
const fingerprint = createVeiledRivenFingerprint(ExportUpgrades[rivenType]);
const upgradeIndex =
@ -57,12 +57,16 @@ export const artifactTransmutationController: RequestHandler = async (req, res)
payload.Consumed.forEach(upgrade => {
const meta = ExportUpgrades[upgrade.ItemType];
counts[meta.rarity] += upgrade.ItemCount;
addMods(inventory, [
{
ItemType: upgrade.ItemType,
ItemCount: upgrade.ItemCount * -1
}
]);
if (upgrade.ItemId.$oid != "000000000000000000000000") {
inventory.Upgrades.pull({ _id: upgrade.ItemId.$oid });
} else {
addMods(inventory, [
{
ItemType: upgrade.ItemType,
ItemCount: upgrade.ItemCount * -1
}
]);
}
if (upgrade.ItemType == "/Lotus/Upgrades/Mods/TransmuteCores/AttackTransmuteCore") {
forcedPolarity = "AP_ATTACK";
} else if (upgrade.ItemType == "/Lotus/Upgrades/Mods/TransmuteCores/DefenseTransmuteCore") {
@ -72,22 +76,33 @@ export const artifactTransmutationController: RequestHandler = async (req, res)
}
});
// Based on the table on https://wiki.warframe.com/w/Transmutation
const weights: Record<TRarity, number> = {
COMMON: counts.COMMON * 95 + counts.UNCOMMON * 15 + counts.RARE * 4,
UNCOMMON: counts.COMMON * 4 + counts.UNCOMMON * 80 + counts.RARE * 10,
RARE: counts.COMMON * 1 + counts.UNCOMMON * 5 + counts.RARE * 50,
LEGENDARY: 0
};
const options: { uniqueName: string; rarity: TRarity }[] = [];
Object.entries(ExportUpgrades).forEach(([uniqueName, upgrade]) => {
if (upgrade.canBeTransmutation && (!forcedPolarity || upgrade.polarity == forcedPolarity)) {
options.push({ uniqueName, rarity: upgrade.rarity });
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
const weights: Record<TRarity, number> = {
COMMON: counts.COMMON * 95 + counts.UNCOMMON * 15 + counts.RARE * 4,
UNCOMMON: counts.COMMON * 4 + counts.UNCOMMON * 80 + counts.RARE * 10,
RARE: counts.COMMON * 1 + counts.UNCOMMON * 5 + counts.RARE * 50,
LEGENDARY: 0
};
const options: { uniqueName: string; rarity: TRarity }[] = [];
Object.entries(ExportUpgrades).forEach(([uniqueName, upgrade]) => {
if (upgrade.canBeTransmutation && (!forcedPolarity || upgrade.polarity == forcedPolarity)) {
options.push({ uniqueName, rarity: upgrade.rarity });
}
});
newModType = getRandomWeightedReward(options, weights)!.uniqueName;
}
const newModType = getRandomWeightedReward(options, weights)!.uniqueName;
addMods(inventory, [
{
ItemType: newModType,
@ -130,3 +145,34 @@ interface IAgnosticUpgradeClient {
ItemCount: number;
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

@ -15,6 +15,12 @@ export const changeDojoRootController: RequestHandler = async (req, res) => {
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> = {};
guild.DojoComponents.forEach(x => {
idToNode[x._id.toString()] = {
@ -43,23 +49,13 @@ export const changeDojoRootController: RequestHandler = async (req, res) => {
newRoot.component.pp = 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];
let i = 0;
const idMap: Record<string, Types.ObjectId> = {};
while (stack.length != 0) {
const top = stack.shift()!;
idMap[top.component._id.toString()] = new Types.ObjectId(
(++i).toString(16).padStart(8, "0") + top.component._id.toString().substr(8)
);
top.component.SortId = new Types.ObjectId();
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));

View File

@ -18,6 +18,7 @@ import {
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { toOid } from "@/src/helpers/inventoryHelpers";
interface IClaimCompletedRecipeRequest {
RecipeIds: IOid[];
@ -80,6 +81,7 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
} else {
logger.debug("Claiming Recipe", { recipe, pendingRecipe });
let BrandedSuits: undefined | IOid[];
if (recipe.secretIngredientAction == "SIA_SPECTRE_LOADOUT_COPY") {
inventory.PendingSpectreLoadouts ??= [];
inventory.SpectreLoadouts ??= [];
@ -104,9 +106,10 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
inventory.BrandedSuits!.findIndex(x => x.equals(pendingRecipe.SuitToUnbrand)),
1
);
BrandedSuits = [toOid(pendingRecipe.SuitToUnbrand!)];
}
let InventoryChanges = {};
let InventoryChanges: IInventoryChanges = {};
if (recipe.consumeOnUse) {
addRecipes(inventory, [
{
@ -130,10 +133,17 @@ export const claimCompletedRecipeController: RequestHandler = async (req, res) =
if (recipe.secretIngredientAction != "SIA_UNBRAND") {
InventoryChanges = {
...InventoryChanges,
...(await addItem(inventory, recipe.resultType, recipe.num, false))
...(await addItem(
inventory,
recipe.resultType,
recipe.num,
false,
undefined,
pendingRecipe.TargetFingerprint
))
};
}
await inventory.save();
res.json({ InventoryChanges });
res.json({ InventoryChanges, BrandedSuits });
}
};

View File

@ -1,4 +1,4 @@
import { getInventory } from "@/src/services/inventoryService";
import { addFusionPoints, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
@ -17,7 +17,7 @@ export const claimLibraryDailyTaskRewardController: RequestHandler = async (req,
}
syndicate.Standing += rewardStanding;
inventory.FusionPoints += 80 * rewardQuantity;
addFusionPoints(inventory, 80 * rewardQuantity);
await inventory.save();
res.json({

View File

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

View File

@ -0,0 +1,41 @@
import { getCalendarProgress, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { getWorldState } from "@/src/services/worldStateService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express";
// GET request; query parameters: CompletedEventIdx=0&Iteration=4&Version=19&Season=CST_SUMMER
export const completeCalendarEventController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const calendarProgress = getCalendarProgress(inventory);
const currentSeason = getWorldState().KnownCalendarSeasons[0];
let inventoryChanges: IInventoryChanges = {};
let dayIndex = 0;
for (const day of currentSeason.Days) {
if (day.events.length == 0 || day.events[0].type != "CET_CHALLENGE") {
if (dayIndex == calendarProgress.SeasonProgress.LastCompletedDayIdx) {
if (day.events.length != 0) {
const selection = day.events[parseInt(req.query.CompletedEventIdx as string)];
if (selection.type == "CET_REWARD") {
inventoryChanges = (await handleStoreItemAcquisition(selection.reward!, inventory))
.InventoryChanges;
} else if (selection.type == "CET_UPGRADE") {
calendarProgress.YearProgress.Upgrades.push(selection.upgrade!);
} else if (selection.type != "CET_PLOT") {
throw new Error(`unexpected selection type: ${selection.type}`);
}
}
break;
}
++dayIndex;
}
}
calendarProgress.SeasonProgress.LastCompletedDayIdx++;
await inventory.save();
res.json({
InventoryChanges: inventoryChanges,
CalendarProgress: inventory.CalendarProgress
});
};

View File

@ -1,8 +1,14 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Guild, GuildMember } from "@/src/models/guildModel";
import { Account } from "@/src/models/loginModel";
import { deleteGuild, getGuildClient, hasGuildPermission, removeDojoKeyItems } from "@/src/services/guildService";
import { addRecipes, combineInventoryChanges, getInventory } from "@/src/services/inventoryService";
import {
deleteGuild,
getGuildClient,
giveClanKey,
hasGuildPermission,
removeDojoKeyItems
} from "@/src/services/guildService";
import { 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";
@ -41,14 +47,7 @@ export const confirmGuildInvitationGetController: RequestHandler = async (req, r
// 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 });
giveClanKey(inventory, inventoryChanges);
await inventory.save();
const guild = (await Guild.findById(req.query.clanId as string))!;
@ -96,14 +95,9 @@ export const confirmGuildInvitationPostController: RequestHandler = async (req,
await GuildMember.deleteMany({ accountId: guildMember.accountId, status: 1 });
// Update inventory of new member
const inventory = await getInventory(guildMember.accountId.toString(), "GuildId Recipes");
const inventory = await getInventory(guildMember.accountId.toString(), "GuildId LevelKeys Recipes");
inventory.GuildId = new Types.ObjectId(req.query.clanId as string);
addRecipes(inventory, [
{
ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint",
ItemCount: 1
}
]);
giveClanKey(inventory);
await inventory.save();
// Add join to clan log

View File

@ -1,9 +1,8 @@
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Guild, GuildMember } from "@/src/models/guildModel";
import { config } from "@/src/services/configService";
import { createMessage } from "@/src/services/inboxService";
import { getInventory } from "@/src/services/inventoryService";
import { Guild } from "@/src/models/guildModel";
import { checkClanAscensionHasRequiredContributors } from "@/src/services/guildService";
import { addFusionPoints, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
import { Types } from "mongoose";
@ -31,49 +30,13 @@ export const contributeGuildClassController: RequestHandler = async (req, res) =
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.
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, [
{
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 checkClanAscensionHasRequiredContributors(guild);
await guild.save();
// Either way, endo is given to the contributor.
const inventory = await getInventory(accountId, "FusionPoints");
inventory.FusionPoints += guild.CeremonyEndo!;
addFusionPoints(inventory, guild.CeremonyEndo!);
await inventory.save();
res.json({

View File

@ -2,8 +2,9 @@ import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Guild, GuildMember } from "@/src/models/guildModel";
import { createUniqueClanName, getGuildClient } from "@/src/services/guildService";
import { addRecipes, getInventory } from "@/src/services/inventoryService";
import { createUniqueClanName, getGuildClient, giveClanKey } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
export const createGuildController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -26,26 +27,15 @@ export const createGuildController: RequestHandler = async (req, res) => {
rank: 0
});
const inventory = await getInventory(accountId, "GuildId Recipes");
const inventory = await getInventory(accountId, "GuildId LevelKeys Recipes");
inventory.GuildId = guild._id;
addRecipes(inventory, [
{
ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint",
ItemCount: 1
}
]);
const inventoryChanges: IInventoryChanges = {};
giveClanKey(inventory, inventoryChanges);
await inventory.save();
res.json({
...(await getGuildClient(guild, accountId)),
InventoryChanges: {
Recipes: [
{
ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint",
ItemCount: 1
}
]
}
InventoryChanges: inventoryChanges
});
};

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,84 @@
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, defaultOverwrites, inventoryChanges);
}
inventoryChanges.CrewShipRawSalvage = [
{
ItemType: payload.ItemType,
ItemCount: -1
}
];
addCrewShipRawSalvage(inventory, inventoryChanges.CrewShipRawSalvage);
await inventory.save();
res.json({
InventoryChanges: inventoryChanges
});
};
interface ICrewShipIdentifySalvageRequest {
ItemType: string;
}

View File

@ -1,5 +1,11 @@
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) => {
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(Date.now() + getRandomInt(3 * 3600 * 1000, 4 * 3600 * 1000));
drone.PendingDamage =
Math.random() < system.damageChance
!config.noResourceExtractorDronesDamage && Math.random() < system.damageChance
? getRandomInt(system.droneDamage.minValue, system.droneDamage.maxValue)
: 0;
const resource = getRandomWeightedRewardUc(system.resources, droneMeta.probabilities)!;

View File

@ -21,10 +21,12 @@ export const entratiLabConquestModeController: RequestHandler = async (req, res)
inventory.EntratiVaultCountResetDate = new Date(weekEnd);
if (inventory.EntratiLabConquestUnlocked) {
inventory.EntratiLabConquestUnlocked = 0;
inventory.EntratiLabConquestCacheScoreMission = 0;
inventory.EntratiLabConquestActiveFrameVariants = [];
}
if (inventory.EchoesHexConquestUnlocked) {
inventory.EchoesHexConquestUnlocked = 0;
inventory.EchoesHexConquestCacheScoreMission = 0;
inventory.EchoesHexConquestActiveFrameVariants = [];
inventory.EchoesHexConquestActiveStickers = [];
}

View File

@ -1,10 +1,9 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper";
import { addMiscItems, getInventory, getStandingLimit, updateStandingLimit } from "@/src/services/inventoryService";
import { addMiscItems, addStanding, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IMiscItem } from "@/src/types/inventoryTypes/inventoryTypes";
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) => {
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 });
}
addMiscItems(inventory, miscItemChanges);
if (gainedStanding && syndicateTag) {
let syndicate = inventory.Affiliations.find(x => x.Tag == syndicateTag);
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);
}
let affiliationMod;
if (gainedStanding && syndicateTag) affiliationMod = addStanding(inventory, syndicateTag, gainedStanding);
await inventory.save();
res.json({
InventoryChanges: {
MiscItems: miscItemChanges
},
SyndicateTag: syndicateTag,
StandingChange: gainedStanding
StandingChange: affiliationMod?.Standing || 0
});
};

View File

@ -104,13 +104,14 @@ export const focusController: RequestHandler = async (req, res) => {
}
case FocusOperation.SentTrainingAmplifier: {
const request = JSON.parse(String(req.body)) as ISentTrainingAmplifierRequest;
const parts: string[] = [
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingGrip",
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingChassis",
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingBarrel"
];
const inventory = await getInventory(accountId);
const inventoryChanges = addEquipment(inventory, "OperatorAmps", request.StartingWeaponType, parts);
const inventoryChanges = addEquipment(inventory, "OperatorAmps", request.StartingWeaponType, {
ModularParts: [
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingGrip",
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingChassis",
"/Lotus/Weapons/Sentients/OperatorAmplifiers/SentTrainingAmplifier/SentAmpTrainingBarrel"
]
});
occupySlot(inventory, InventorySlot.AMPS, false);
await inventory.save();
res.json((inventoryChanges.OperatorAmps as IEquipmentClient[])[0]);

View File

@ -0,0 +1,84 @@
import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addMiscItem, getInventory } from "@/src/services/inventoryService";
import { toStoreItem } from "@/src/services/itemDataService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { createGarden, getPersonalRooms } from "@/src/services/personalRoomsService";
import { IMongoDate } from "@/src/types/commonTypes";
import { IMissionReward } from "@/src/types/missionTypes";
import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { IGardeningClient } from "@/src/types/shipTypes";
import { RequestHandler } from "express";
import { dict_en, ExportResources } from "warframe-public-export-plus";
export const gardeningController: RequestHandler = async (req, res) => {
const data = getJSONfromString<IGardeningRequest>(String(req.body));
if (data.Mode != "HarvestAll") {
throw new Error(`unexpected gardening mode: ${data.Mode}`);
}
const accountId = await getAccountIdForRequest(req);
const [inventory, personalRooms] = await Promise.all([
getInventory(accountId, "MiscItems"),
getPersonalRooms(accountId, "Apartment")
]);
// Harvest plants
const inventoryChanges: IInventoryChanges = {};
const rewards: Record<string, IMissionReward[][]> = {};
for (const planter of personalRooms.Apartment.Gardening.Planters) {
rewards[planter.Name] = [];
for (const plant of planter.Plants) {
const itemType =
"/Lotus/Types/Gameplay/Duviri/Resource/DuviriPlantItem" +
plant.PlantType.substring(plant.PlantType.length - 1);
const itemCount = Math.random() < 0.775 ? 2 : 4;
addMiscItem(inventory, itemType, itemCount, inventoryChanges);
rewards[planter.Name].push([
{
StoreItem: toStoreItem(itemType),
TypeName: itemType,
ItemCount: itemCount,
DailyCooldown: false,
Rarity: itemCount == 2 ? 0.7743589743589744 : 0.22564102564102564,
TweetText: `${itemCount}x ${dict_en[ExportResources[itemType].name]} (Resource)`,
ProductCategory: "MiscItems"
}
]);
}
}
// Refresh garden
personalRooms.Apartment.Gardening = createGarden();
await Promise.all([inventory.save(), personalRooms.save()]);
const planter = personalRooms.Apartment.Gardening.Planters[personalRooms.Apartment.Gardening.Planters.length - 1];
const plant = planter.Plants[planter.Plants.length - 1];
res.json({
GardenTagName: planter.Name,
PlantType: plant.PlantType,
PlotIndex: plant.PlotIndex,
EndTime: toMongoDate(plant.EndTime),
InventoryChanges: inventoryChanges,
Gardening: personalRooms.toJSON<IPersonalRoomsClient>().Apartment.Gardening,
Rewards: rewards
} satisfies IGardeningResponse);
};
interface IGardeningRequest {
Mode: string;
}
interface IGardeningResponse {
GardenTagName: string;
PlantType: string;
PlotIndex: number;
EndTime: IMongoDate;
InventoryChanges: IInventoryChanges;
Gardening: IGardeningClient;
Rewards: Record<string, IMissionReward[][]>;
}

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";
const getIgnoredUsersController: RequestHandler = (_req, res) => {
res.writeHead(200, {
"Content-Type": "text/html",
"Content-Length": "3"
export const getIgnoredUsersController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const ignores = await Ignore.find({ ignorer: accountId });
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([
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
])
);
res.json({ IgnoredUsers: ignoredUsers });
};
export { getIgnoredUsersController };

View File

@ -1,14 +1,12 @@
import { Inventory } from "@/src/models/inventoryModels/inventoryModel";
import { generateRewardSeed } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express";
export const getNewRewardSeedController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const rewardSeed = generateRewardSeed();
logger.debug(`generated new reward seed: ${rewardSeed}`);
await Inventory.updateOne(
{
accountOwnerId: accountId

View File

@ -2,17 +2,24 @@ import { RequestHandler } from "express";
import { config } from "@/src/services/configService";
import allShipFeatures from "@/static/fixed_responses/allShipFeatures.json";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getPersonalRooms } from "@/src/services/personalRoomsService";
import { createGarden, getPersonalRooms } from "@/src/services/personalRoomsService";
import { getShip } from "@/src/services/shipService";
import { toOid } from "@/src/helpers/inventoryHelpers";
import { IGetShipResponse } from "@/src/types/shipTypes";
import { IPersonalRooms } from "@/src/types/personalRoomsTypes";
import { IPersonalRoomsClient } from "@/src/types/personalRoomsTypes";
import { getLoadout } from "@/src/services/loadoutService";
export const getShipController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const personalRoomsDb = await getPersonalRooms(accountId);
const personalRooms = personalRoomsDb.toJSON<IPersonalRooms>();
// Setup gardening if it's missing. Maybe should be done as part of some quest completion in the future.
if (personalRoomsDb.Apartment.Gardening.Planters.length == 0) {
personalRoomsDb.Apartment.Gardening = createGarden();
await personalRoomsDb.save();
}
const personalRooms = personalRoomsDb.toJSON<IPersonalRoomsClient>();
const loadout = await getLoadout(accountId);
const ship = await getShip(personalRoomsDb.activeShipId, "ShipAttachments SkinFlavourItem");
@ -26,7 +33,10 @@ export const getShipController: RequestHandler = async (req, res) => {
Colors: personalRooms.ShipInteriorColors,
ShipAttachments: ship.ShipAttachments,
SkinFlavourItem: ship.SkinFlavourItem
}
},
FavouriteLoadoutId: personalRooms.Ship.FavouriteLoadoutId
? toOid(personalRooms.Ship.FavouriteLoadoutId)
: undefined
},
Apartment: personalRooms.Apartment,
TailorShop: personalRooms.TailorShop

View File

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

View File

@ -16,7 +16,7 @@ export const giveQuestKeyRewardController: RequestHandler = async (req, res) =>
const inventory = await getInventory(accountId);
const inventoryChanges = await addItem(inventory, reward.ItemType, reward.Amount);
await inventory.save();
res.json(inventoryChanges.InventoryChanges);
res.json(inventoryChanges);
//TODO: consider whishlist changes
};

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

@ -14,29 +14,33 @@ import {
import { ExportDojoRecipes } from "warframe-public-export-plus";
import { getAccountIdForRequest } from "@/src/services/loginService";
import {
addCrewShipWeaponSkin,
addEquipment,
addItem,
addMiscItems,
addRecipes,
combineInventoryChanges,
getInventory,
occupySlot,
updateCurrency
} 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 { config } from "@/src/services/configService";
import { GuildPermission, ITechProjectClient } from "@/src/types/guildTypes";
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 { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
export const guildTechController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const guild = await getGuildForRequestEx(req, inventory);
const data = JSON.parse(String(req.body)) as TGuildTechRequest;
if (data.Action == "Sync") {
let needSave = false;
const techProjects: ITechProjectClient[] = [];
const guild = await getGuildForRequestEx(req, inventory);
if (guild.TechProjects) {
for (const project of guild.TechProjects) {
const techProject: ITechProjectClient = {
@ -59,137 +63,224 @@ export const guildTechController: RequestHandler = async (req, res) => {
}
res.json({ TechProjects: techProjects });
} else if (data.Action == "Start") {
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
res.status(400).send("-1").end();
return;
}
const recipe = ExportDojoRecipes.research[data.RecipeType];
guild.TechProjects ??= [];
if (!guild.TechProjects.find(x => x.ItemType == data.RecipeType)) {
const techProject =
guild.TechProjects[
guild.TechProjects.push({
ItemType: data.RecipeType,
ReqCredits: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, recipe.price),
ReqItems: recipe.ingredients.map(x => ({
ItemType: x.ItemType,
ItemCount: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, x.ItemCount)
})),
State: 0
}) - 1
];
setGuildTechLogState(guild, techProject.ItemType, 5);
if (config.noDojoResearchCosts) {
processFundedGuildTechProject(guild, techProject, recipe);
} else {
if (data.RecipeType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/") {
guild.ActiveDojoColorResearch = data.RecipeType;
if (data.Mode == "Guild") {
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
res.status(400).send("-1").end();
return;
}
const recipe = ExportDojoRecipes.research[data.RecipeType];
guild.TechProjects ??= [];
if (!guild.TechProjects.find(x => x.ItemType == data.RecipeType)) {
const techProject =
guild.TechProjects[
guild.TechProjects.push({
ItemType: data.RecipeType,
ReqCredits: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, recipe.price),
ReqItems: recipe.ingredients.map(x => ({
ItemType: x.ItemType,
ItemCount: config.noDojoResearchCosts ? 0 : scaleRequiredCount(guild.Tier, x.ItemCount)
})),
State: 0
}) - 1
];
setGuildTechLogState(guild, techProject.ItemType, 5);
if (config.noDojoResearchCosts) {
processFundedGuildTechProject(guild, techProject, recipe);
} else {
if (data.RecipeType.substring(0, 39) == "/Lotus/Types/Items/Research/DojoColors/") {
guild.ActiveDojoColorResearch = data.RecipeType;
}
}
}
}
await guild.save();
res.end();
} else if (data.Action == "Contribute") {
if (!hasAccessToDojo(inventory)) {
res.status(400).send("-1").end();
return;
}
const guildMember = (await GuildMember.findOne(
{ accountId, guildId: guild._id },
"RegularCreditsContributed MiscItemsContributed"
))!;
const contributions = data;
const techProject = guild.TechProjects!.find(x => x.ItemType == contributions.RecipeType)!;
if (contributions.VaultCredits) {
if (contributions.VaultCredits > techProject.ReqCredits) {
contributions.VaultCredits = techProject.ReqCredits;
await guild.save();
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`
);
}
}
techProject.ReqCredits -= contributions.VaultCredits;
guild.VaultRegularCredits! -= contributions.VaultCredits;
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") {
if ((req.query.guildId as string) == "000000000000000000000000") {
const techProject = inventory.PersonalTechProjects.id(data.ResearchId)!;
if (contributions.RegularCredits > techProject.ReqCredits) {
contributions.RegularCredits = techProject.ReqCredits;
}
techProject.ReqCredits -= contributions.RegularCredits;
techProject.ReqCredits -= data.RegularCredits;
const inventoryChanges: IInventoryChanges = updateCurrency(inventory, data.RegularCredits, false);
guildMember.RegularCreditsContributed ??= 0;
guildMember.RegularCreditsContributed += contributions.RegularCredits;
if (contributions.VaultMiscItems.length) {
for (const miscItem of contributions.VaultMiscItems) {
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;
const vaultMiscItem = guild.VaultMiscItems!.find(x => x.ItemType == miscItem.ItemType)!;
vaultMiscItem.ItemCount -= miscItem.ItemCount;
miscItemChanges.push({
ItemType: miscItem.ItemType,
ItemCount: miscItem.ItemCount * -1
});
}
}
}
addMiscItems(inventory, miscItemChanges);
inventoryChanges.MiscItems = miscItemChanges;
const miscItemChanges = [];
for (const miscItem of contributions.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
});
techProject.HasContributions = true;
addGuildMemberMiscItemContribution(guildMember, miscItem);
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)) {
res.status(400).send("-1").end();
return;
}
const guild = await getGuildForRequestEx(req, inventory);
const guildMember = (await GuildMember.findOne(
{ accountId, guildId: guild._id },
"RegularCreditsContributed MiscItemsContributed"
))!;
const techProject = guild.TechProjects!.find(x => x.ItemType == data.RecipeType)!;
if (data.VaultCredits) {
if (data.VaultCredits > techProject.ReqCredits) {
data.VaultCredits = techProject.ReqCredits;
}
techProject.ReqCredits -= data.VaultCredits;
guild.VaultRegularCredits! -= data.VaultCredits;
}
if (data.RegularCredits > techProject.ReqCredits) {
data.RegularCredits = techProject.ReqCredits;
}
techProject.ReqCredits -= data.RegularCredits;
guildMember.RegularCreditsContributed ??= 0;
guildMember.RegularCreditsContributed += data.RegularCredits;
if (data.VaultMiscItems.length) {
for (const miscItem of data.VaultMiscItems) {
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;
const vaultMiscItem = guild.VaultMiscItems!.find(x => x.ItemType == miscItem.ItemType)!;
vaultMiscItem.ItemCount -= miscItem.ItemCount;
}
}
}
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
});
addGuildMemberMiscItemContribution(guildMember, miscItem);
}
}
addMiscItems(inventory, miscItemChanges);
const inventoryChanges: IInventoryChanges = updateCurrency(inventory, data.RegularCredits, false);
inventoryChanges.MiscItems = miscItemChanges;
// Check if research is fully funded now.
await processGuildTechProjectContributionsUpdate(guild, techProject);
await Promise.all([guild.save(), inventory.save(), guildMember.save()]);
res.json({
InventoryChanges: inventoryChanges,
Vault: getGuildVault(guild)
});
}
addMiscItems(inventory, miscItemChanges);
const inventoryChanges: IInventoryChanges = updateCurrency(inventory, contributions.RegularCredits, false);
inventoryChanges.MiscItems = miscItemChanges;
// Check if research is fully funded now.
await processGuildTechProjectContributionsUpdate(guild, techProject);
await Promise.all([guild.save(), inventory.save(), guildMember.save()]);
res.json({
InventoryChanges: inventoryChanges,
Vault: getGuildVault(guild)
});
} else if (data.Action.split(",")[0] == "Buy") {
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) {
res.status(400).send("-1").end();
return;
}
const purchase = data as IGuildTechBuyRequest;
const quantity = parseInt(data.Action.split(",")[1]);
const recipeChanges = [
{
ItemType: purchase.RecipeType,
ItemCount: quantity
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();
return;
}
];
addRecipes(inventory, recipeChanges);
const currencyChanges = updateCurrency(
inventory,
ExportDojoRecipes.research[purchase.RecipeType].replicatePrice,
false
);
await inventory.save();
// Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`.
res.json({
inventoryChanges: {
...currencyChanges,
Recipes: recipeChanges
}
});
const quantity = parseInt(data.Action.split(",")[1]);
const recipeChanges = [
{
ItemType: purchase.RecipeType,
ItemCount: quantity
}
];
addRecipes(inventory, recipeChanges);
const currencyChanges = updateCurrency(
inventory,
ExportDojoRecipes.research[purchase.RecipeType].replicatePrice,
false
);
await inventory.save();
// Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`.
res.json({
inventoryChanges: {
...currencyChanges,
Recipes: recipeChanges
}
});
} else {
const inventoryChanges = claimSalvagedComponent(inventory, purchase.CategoryItemId!);
await inventory.save();
res.json({
inventoryChanges: inventoryChanges
});
}
} else if (data.Action == "Fabricate") {
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Fabricator))) {
res.status(400).send("-1").end();
return;
@ -206,6 +297,7 @@ export const guildTechController: RequestHandler = async (req, res) => {
// Not a mistake: This response uses `inventoryChanges` instead of `InventoryChanges`.
res.json({ inventoryChanges: inventoryChanges });
} else if (data.Action == "Pause") {
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
res.status(400).send("-1").end();
return;
@ -217,6 +309,7 @@ export const guildTechController: RequestHandler = async (req, res) => {
await removePigmentsFromGuildMembers(guild._id);
res.end();
} else if (data.Action == "Unpause") {
const guild = await getGuildForRequestEx(req, inventory);
if (!hasAccessToDojo(inventory) || !(await hasGuildPermission(guild, accountId, GuildPermission.Tech))) {
res.status(400).send("-1").end();
return;
@ -226,9 +319,66 @@ export const guildTechController: RequestHandler = async (req, res) => {
guild.ActiveDojoColorResearch = data.RecipeType;
await guild.save();
res.end();
} else if (data.Action == "Cancel" && data.CategoryItemId) {
const personalTechProjectIndex = inventory.PersonalTechProjects.findIndex(x =>
x.CategoryItemId?.equals(data.CategoryItemId)
);
const personalTechProject = inventory.PersonalTechProjects[personalTechProjectIndex];
inventory.PersonalTechProjects.splice(personalTechProjectIndex, 1);
const meta = ExportDojoRecipes.research[personalTechProject.ItemType];
const contributedCredits = meta.price - personalTechProject.ReqCredits;
const inventoryChanges = updateCurrency(inventory, contributedCredits * -1, false);
inventoryChanges.MiscItems = [];
for (const ingredient of meta.ingredients) {
const reqItem = personalTechProject.ReqItems.find(x => x.ItemType == ingredient.ItemType);
if (reqItem) {
const contributedItems = ingredient.ItemCount - reqItem.ItemCount;
inventoryChanges.MiscItems.push({
ItemType: ingredient.ItemType,
ItemCount: contributedItems
});
}
}
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(`unknown guildTech action: ${data.Action}`);
throw new Error(`unhandled guildTech request`);
}
};
@ -238,23 +388,70 @@ type TGuildTechRequest =
| IGuildTechContributeRequest;
interface IGuildTechBasicRequest {
Action: "Start" | "Fabricate" | "Pause" | "Unpause";
Mode: "Guild";
Action: "Start" | "Fabricate" | "Pause" | "Unpause" | "Cancel" | "Rush" | "InstantFinish";
Mode: "Guild" | "Personal";
RecipeType: string;
TechProductCategory?: string;
CategoryItemId?: string;
}
interface IGuildTechBuyRequest {
interface IGuildTechBuyRequest extends Omit<IGuildTechBasicRequest, "Action"> {
Action: string;
Mode: "Guild";
RecipeType: string;
}
interface IGuildTechContributeRequest {
Action: "Contribute";
ResearchId: "";
ResearchId: string;
RecipeType: string;
RegularCredits: number;
MiscItems: IMiscItem[];
VaultCredits: number;
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, {
UpgradeFingerprint: salvageItem.UpgradeFingerprint
})),
...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, false)
};
inventoryChanges.RemovedIdItems = [
{
ItemId: { $oid: itemId }
}
];
return inventoryChanges;
};

View File

@ -13,6 +13,7 @@ import { addItems, combineInventoryChanges, getInventory } from "@/src/services/
import { logger } from "@/src/utils/logger";
import { ExportFlavour, ExportGear } from "warframe-public-export-plus";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { fromStoreItem, isStoreItem } from "@/src/services/itemDataService";
export const inboxController: RequestHandler = async (req, res) => {
const { deleteId, lastMessage: latestClientMessageId, messageId } = req.query;
@ -34,8 +35,8 @@ export const inboxController: RequestHandler = async (req, res) => {
message.r = true;
await message.save();
const attachmentItems = message.att;
const attachmentCountedItems = message.countedAtt;
const attachmentItems = message.attVisualOnly ? undefined : message.att;
const attachmentCountedItems = message.attVisualOnly ? undefined : message.countedAtt;
if (!attachmentItems && !attachmentCountedItems && !message.gifts) {
res.status(200).end();
@ -48,7 +49,7 @@ export const inboxController: RequestHandler = async (req, res) => {
await addItems(
inventory,
attachmentItems.map(attItem => ({
ItemType: attItem,
ItemType: isStoreItem(attItem) ? fromStoreItem(attItem) : attItem,
ItemCount: attItem in ExportGear ? (ExportGear[attItem].purchaseQuantity ?? 1) : 1
})),
inventoryChanges

View File

@ -14,9 +14,16 @@ import {
ExportVirtuals
} from "warframe-public-export-plus";
import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "@/src/services/infestedFoundryService";
import { addMiscItems, allDailyAffiliationKeys, createLibraryDailyTask } from "@/src/services/inventoryService";
import {
addMiscItems,
allDailyAffiliationKeys,
cleanupInventory,
createLibraryDailyTask,
generateRewardSeed
} from "@/src/services/inventoryService";
import { logger } from "@/src/utils/logger";
import { catBreadHash } from "@/src/helpers/stringHelpers";
import { Types } from "mongoose";
export const inventoryController: RequestHandler = async (request, response) => {
const accountId = await getAccountIdForRequest(request);
@ -79,8 +86,10 @@ export const inventoryController: RequestHandler = async (request, response) =>
}
}
cleanupInventory(inventory);
inventory.NextRefill = new Date((Math.trunc(Date.now() / 86400000) + 1) * 86400000);
await inventory.save();
//await inventory.save();
}
if (
@ -89,9 +98,20 @@ export const inventoryController: RequestHandler = async (request, response) =>
new Date() >= inventory.InfestedFoundry.AbilityOverrideUnlockCooldown
) {
handleSubsumeCompletion(inventory);
await inventory.save();
//await inventory.save();
}
if (inventory.LastInventorySync) {
const lastSyncDuviriMood = Math.trunc(inventory.LastInventorySync.getTimestamp().getTime() / 7200000);
const currentDuviriMood = Math.trunc(Date.now() / 7200000);
if (lastSyncDuviriMood != currentDuviriMood) {
logger.debug(`refreshing duviri seed`);
inventory.DuviriInfo.Seed = generateRewardSeed();
}
}
inventory.LastInventorySync = new Types.ObjectId();
await inventory.save();
response.json(await getInventoryResponse(inventory, "xpBasedLevelCapDisabled" in request.query));
};
@ -149,7 +169,7 @@ export const getInventoryResponse = async (
inventoryResponse.ShipDecorations = [];
for (const [uniqueName, item] of Object.entries(ExportResources)) {
if (item.productCategory == "ShipDecorations") {
inventoryResponse.ShipDecorations.push({ ItemType: uniqueName, ItemCount: 1 });
inventoryResponse.ShipDecorations.push({ ItemType: uniqueName, ItemCount: 999_999 });
}
}
}
@ -202,7 +222,8 @@ export const getInventoryResponse = async (
if (config.universalPolarityEverywhere) {
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({
Slot: i,
Value: ArtifactPolarity.Any
@ -257,12 +278,16 @@ export const getInventoryResponse = async (
}
}
if (config.noDailyFocusLimit) {
inventoryResponse.DailyFocus = Math.max(999_999, 250000 + inventoryResponse.PlayerLevel * 5000);
}
if (inventoryResponse.InfestedFoundry) {
applyCheatsToInfestedFoundry(inventoryResponse.InfestedFoundry);
}
// Omitting this field so opening the navigation resyncs the inventory which is more desirable for typical usage.
//inventoryResponse.LastInventorySync = toOid(new Types.ObjectId());
inventoryResponse.LastInventorySync = undefined;
// Set 2FA enabled so trading post can be used
inventoryResponse.HWIDProtectEnabled = true;

View File

@ -21,7 +21,11 @@ export const loginController: RequestHandler = async (request, response) => {
const myAddress = request.host.indexOf("warframe.com") == -1 ? request.host : config.myAddress;
if (!account && config.autoCreateAccount && loginRequest.ClientType != "webui") {
if (
!account &&
((config.autoCreateAccount && loginRequest.ClientType != "webui") ||
loginRequest.ClientType == "webui-register")
) {
try {
const nameFromEmail = loginRequest.email.substring(0, loginRequest.email.indexOf("@"));
let name = nameFromEmail || loginRequest.email.substring(1) || "SpaceNinja";
@ -37,7 +41,7 @@ export const loginController: RequestHandler = async (request, response) => {
password: loginRequest.password,
DisplayName: name,
CountryCode: loginRequest.lang.toUpperCase(),
ClientType: loginRequest.ClientType,
ClientType: loginRequest.ClientType == "webui-register" ? "webui" : loginRequest.ClientType,
CrossPlatformAllowed: true,
ForceLogoutVersion: 0,
ConsentNeeded: false,
@ -59,6 +63,11 @@ export const loginController: RequestHandler = async (request, response) => {
return;
}
if (loginRequest.ClientType == "webui-register") {
response.status(400).json({ error: "account already exists" });
return;
}
if (!isCorrectPassword(loginRequest.password, account.password)) {
response.status(400).json({ error: "incorrect login data" });
return;

View File

@ -3,9 +3,10 @@ import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IMissionInventoryUpdateRequest } from "@/src/types/requestTypes";
import { addMissionInventoryUpdates, addMissionRewards } from "@/src/services/missionInventoryUpdateService";
import { getInventory } from "@/src/services/inventoryService";
import { generateRewardSeed, getInventory } from "@/src/services/inventoryService";
import { getInventoryResponse } from "./inventoryController";
import { logger } from "@/src/utils/logger";
import { IMissionInventoryUpdateResponse } from "@/src/types/missionTypes";
/*
**** INPUT ****
@ -53,9 +54,16 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
logger.debug("mission report:", missionReport);
const inventory = await getInventory(accountId);
const firstCompletion = missionReport.SortieId
? inventory.CompletedSorties.indexOf(missionReport.SortieId) == -1
: false;
const inventoryUpdates = await addMissionInventoryUpdates(inventory, missionReport);
if (missionReport.MissionStatus !== "GS_SUCCESS") {
if (
missionReport.MissionStatus !== "GS_SUCCESS" &&
!(missionReport.RewardInfo?.jobId || missionReport.RewardInfo?.challengeMissionId)
) {
inventory.RewardSeed = generateRewardSeed();
await inventory.save();
const inventoryResponse = await getInventoryResponse(inventory, true);
res.json({
@ -65,8 +73,16 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
return;
}
const { MissionRewards, inventoryChanges, credits } = await addMissionRewards(inventory, missionReport);
const {
MissionRewards,
inventoryChanges,
credits,
AffiliationMods,
SyndicateXPItemReward,
ConquestCompletedMissionsCount
} = await addMissionRewards(inventory, missionReport, firstCompletion);
inventory.RewardSeed = generateRewardSeed();
await inventory.save();
const inventoryResponse = await getInventoryResponse(inventory, true);
@ -77,8 +93,11 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
MissionRewards,
...credits,
...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,
ConquestCompletedMissionsCount
} satisfies IMissionInventoryUpdateResponse);
};
/*

View File

@ -17,12 +17,13 @@ import { getDefaultUpgrades } from "@/src/services/itemDataService";
import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper";
import { IEquipmentDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { getRandomInt } from "@/src/services/rngService";
import { ExportSentinels } from "warframe-public-export-plus";
import { ExportSentinels, ExportWeapons, IDefaultUpgrade } from "warframe-public-export-plus";
import { Status } from "@/src/types/inventoryTypes/inventoryTypes";
interface IModularCraftRequest {
WeaponType: string;
Parts: string[];
isWebUi?: boolean;
}
export const modularWeaponCraftingController: RequestHandler = async (req, res) => {
@ -34,9 +35,9 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res)
const category = modularWeaponTypes[data.WeaponType];
const inventory = await getInventory(accountId);
const defaultUpgrades = getDefaultUpgrades(data.Parts);
let defaultUpgrades: IDefaultUpgrade[] | undefined;
const defaultOverwrites: Partial<IEquipmentDatabase> = {
Configs: applyDefaultUpgrades(inventory, defaultUpgrades)
ModularParts: data.Parts
};
const inventoryChanges: IInventoryChanges = {};
if (category == "KubrowPets") {
@ -129,38 +130,63 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res)
// Only save mutagen & antigen in the ModularParts.
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);
}
defaultUpgrades = meta.defaultUpgrades;
} else {
defaultUpgrades = getDefaultUpgrades(data.Parts);
}
addEquipment(inventory, category, data.WeaponType, data.Parts, inventoryChanges, defaultOverwrites);
combineInventoryChanges(inventoryChanges, occupySlot(inventory, productCategoryToInventoryBin(category)!, false));
if (category == "MoaPets") {
const weapon = ExportSentinels[data.WeaponType].defaultWeapon;
if (weapon) {
const category = ExportWeapons[weapon].productCategory;
addEquipment(inventory, category, weapon, undefined, inventoryChanges);
combineInventoryChanges(
inventoryChanges,
occupySlot(inventory, productCategoryToInventoryBin(category)!, !!data.isWebUi)
);
}
}
defaultOverwrites.Configs = applyDefaultUpgrades(inventory, defaultUpgrades);
addEquipment(inventory, category, data.WeaponType, defaultOverwrites, inventoryChanges);
combineInventoryChanges(
inventoryChanges,
occupySlot(inventory, productCategoryToInventoryBin(category)!, !!data.isWebUi)
);
if (defaultUpgrades) {
inventoryChanges.RawUpgrades = defaultUpgrades.map(x => ({ ItemType: x.ItemType, ItemCount: 1 }));
}
// Remove credits & parts
const miscItemChanges = [];
for (const part of data.Parts) {
miscItemChanges.push({
ItemType: part,
ItemCount: -1
});
let currencyChanges = {};
if (!data.isWebUi) {
for (const part of data.Parts) {
miscItemChanges.push({
ItemType: part,
ItemCount: -1
});
}
currencyChanges = updateCurrency(
inventory,
category == "Hoverboards" ||
category == "MoaPets" ||
category == "LongGuns" ||
category == "Pistols" ||
category == "KubrowPets"
? 5000
: 4000, // Definitely correct for Melee & OperatorAmps
false
);
addMiscItems(inventory, miscItemChanges);
}
const currencyChanges = updateCurrency(
inventory,
category == "Hoverboards" ||
category == "MoaPets" ||
category == "LongGuns" ||
category == "Pistols" ||
category == "KubrowPets"
? 5000
: 4000, // Definitely correct for Melee & OperatorAmps
false
);
addMiscItems(inventory, miscItemChanges);
await inventory.save();
await inventory.save();
// Tell client what we did
res.json({
InventoryChanges: {

View File

@ -21,7 +21,11 @@ import { IInventoryChanges } from "@/src/types/purchaseTypes";
export const modularWeaponSaleController: RequestHandler = async (req, res) => {
const partTypeToParts: Record<string, string[]> = {};
for (const [uniqueName, data] of Object.entries(ExportWeapons)) {
if (data.partType && data.premiumPrice) {
if (
data.partType &&
data.premiumPrice &&
!data.excludeFromCodex // exclude pvp variants
) {
partTypeToParts[data.partType] ??= [];
partTypeToParts[data.partType].push(uniqueName);
}
@ -41,24 +45,18 @@ export const modularWeaponSaleController: RequestHandler = async (req, res) => {
const defaultUpgrades = getDefaultUpgrades(weaponInfo.ModularParts);
const configs = applyDefaultUpgrades(inventory, defaultUpgrades);
const inventoryChanges: IInventoryChanges = {
...addEquipment(
inventory,
category,
weaponInfo.ItemType,
weaponInfo.ModularParts,
{},
{
Features: EquipmentFeatures.DOUBLE_CAPACITY | EquipmentFeatures.GILDED,
ItemName: payload.ItemName,
Configs: configs,
Polarity: [
{
Slot: payload.PolarizeSlot,
Value: payload.PolarizeValue
}
]
}
),
...addEquipment(inventory, category, weaponInfo.ItemType, {
Features: EquipmentFeatures.DOUBLE_CAPACITY | EquipmentFeatures.GILDED,
ItemName: payload.ItemName,
Configs: configs,
ModularParts: weaponInfo.ModularParts,
Polarity: [
{
Slot: payload.PolarizeSlot,
Value: payload.PolarizeValue
}
]
}),
...occupySlot(inventory, productCategoryToInventoryBin(category)!, true),
...updateCurrency(inventory, weaponInfo.PremiumPrice, true)
};
@ -143,7 +141,7 @@ const getModularWeaponSale = (
getItemType: (parts: string[]) => string
): IModularWeaponSaleInfo => {
const rng = new CRng(day);
const parts = partTypes.map(partType => rng.randomElement(partTypeToParts[partType]));
const parts = partTypes.map(partType => rng.randomElement(partTypeToParts[partType])!);
let partsCost = 0;
for (const part of parts) {
partsCost += ExportWeapons[part].premiumPrice!;

View File

@ -1,10 +1,31 @@
import { getInfNodes, getNemesisPasscode } from "@/src/helpers/nemesisHelpers";
import {
consumeModCharge,
encodeNemesisGuess,
getInfNodes,
getKnifeUpgrade,
getNemesisPasscode,
getNemesisPasscodeModTypes,
getWeaponsForManifest,
IKnifeResponse,
showdownNodes
} from "@/src/helpers/nemesisHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
import { freeUpSlot, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { SRng } from "@/src/services/rngService";
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,
IInventoryClient,
INemesisClient,
InventorySlot,
IUpgradeClient,
IWeaponSkinClient,
LoadoutIndex,
TEquipmentKey
} from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express";
@ -49,7 +70,7 @@ export const nemesisController: RequestHandler = async (req, res) => {
} else if ((req.query.mode as string) == "p") {
const inventory = await getInventory(accountId, "Nemesis");
const body = getJSONfromString<INemesisPrespawnCheckRequest>(String(req.body));
const passcode = getNemesisPasscode(inventory.Nemesis!.fp, inventory.Nemesis!.Faction);
const passcode = getNemesisPasscode(inventory.Nemesis!);
let guessResult = 0;
if (inventory.Nemesis!.Faction == "FC_INFESTATION") {
for (let i = 0; i != 3; ++i) {
@ -66,6 +87,83 @@ export const nemesisController: RequestHandler = async (req, res) => {
}
}
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 if correct antivirus mod is installed
const response: IKnifeResponse = {};
if (result1 == 0 || result2 == 0 || result3 == 0) {
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!;
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 = 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") {
const inventory = await getInventory(accountId, "Nemesis");
const body = getJSONfromString<INemesisStartRequest>(String(req.body));
@ -73,18 +171,7 @@ export const nemesisController: RequestHandler = async (req, res) => {
let weaponIdx = -1;
if (body.target.Faction != "FC_INFESTATION") {
let weapons: readonly string[];
if (body.target.manifest == "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionSix") {
weapons = kuvaLichVersionSixWeapons;
} else if (
body.target.manifest == "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionFour" ||
body.target.manifest == "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionThree"
) {
weapons = corpusVersionThreeWeapons;
} else {
throw new Error(`unknown nemesis manifest: ${body.target.manifest}`);
}
const weapons = getWeaponsForManifest(body.target.manifest);
const initialWeaponIdx = new SRng(body.target.fp).randomInt(0, weapons.length - 1);
weaponIdx = initialWeaponIdx;
do {
@ -126,6 +213,38 @@ export const nemesisController: RequestHandler = async (req, res) => {
res.json({
target: inventory.toJSON().Nemesis
});
} else if ((req.query.mode as string) == "w") {
const inventory = await getInventory(
accountId,
"Nemesis LoadOutPresets CurrentLoadOutIds DataKnives Upgrades RawUpgrades"
);
//const body = getJSONfromString<INemesisWeakenRequest>(String(req.body));
inventory.Nemesis!.InfNodes = [
{
Node: showdownNodes[inventory.Nemesis!.Faction],
Influence: 1
}
];
inventory.Nemesis!.Weakened = true;
const response: IKnifeResponse & { target: INemesisClient } = {
target: inventory.toJSON<IInventoryClient>().Nemesis!
};
// Consume charge of the correct requiem mod(s)
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 modTypes = getNemesisPasscodeModTypes(inventory.Nemesis!);
for (const modType of modTypes) {
const upgrade = getKnifeUpgrade(inventory, dataknifeUpgrades, modType);
consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
}
await inventory.save();
res.json(response);
} else {
logger.debug(`data provided to ${req.path}: ${String(req.body)}`);
throw new Error(`unknown nemesis mode: ${String(req.query.mode)}`);
@ -173,38 +292,23 @@ interface INemesisPrespawnCheckRequest {
potency?: number[];
}
const kuvaLichVersionSixWeapons = [
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Drakgoon/KuvaDrakgoon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Karak/KuvaKarak",
"/Lotus/Weapons/Grineer/Melee/GrnKuvaLichScythe/GrnKuvaLichScytheWeapon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Kohm/KuvaKohm",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Ogris/KuvaOgris",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Quartakk/KuvaQuartakk",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Tonkor/KuvaTonkor",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Brakk/KuvaBrakk",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Kraken/KuvaKraken",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Seer/KuvaSeer",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Stubba/KuvaStubba",
"/Lotus/Weapons/Grineer/HeavyWeapons/GrnHeavyGrenadeLauncher",
"/Lotus/Weapons/Grineer/LongGuns/GrnKuvaLichRifle/GrnKuvaLichRifleWeapon",
"/Lotus/Weapons/Grineer/Bows/GrnBow/GrnBowWeapon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hind/KuvaHind",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Nukor/KuvaNukor",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hek/KuvaHekWeapon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Zarr/KuvaZarr",
"/Lotus/Weapons/Grineer/KuvaLich/HeavyWeapons/Grattler/KuvaGrattler",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Sobek/KuvaSobek"
];
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?: IKnife;
}
const corpusVersionThreeWeapons = [
"/Lotus/Weapons/Corpus/LongGuns/CrpBriefcaseLauncher/CrpBriefcaseLauncher",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEArcaPlasmor/CrpBEArcaPlasmor",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEFluxRifle/CrpBEFluxRifle",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBETetra/CrpBETetra",
"/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBECycron/CrpBECycron",
"/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEDetron/CrpBEDetron",
"/Lotus/Weapons/Corpus/Pistols/CrpIgniterPistol/CrpIgniterPistol",
"/Lotus/Weapons/Corpus/Pistols/CrpBriefcaseAkimbo/CrpBriefcaseAkimboPistol",
"/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEPlinx/CrpBEPlinxWeapon",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEGlaxion/CrpBEGlaxion"
];
// interface INemesisWeakenRequest {
// target: INemesisClient;
// knife: IKnife;
// }
interface IKnife {
Item: IEquipmentClient;
Skins: IWeaponSkinClient[];
ModSlot: number;
CustSlot: number;
AttachedUpgrades: IUpgradeClient[];
HiddenWhenHolstered: boolean;
}

View File

@ -13,6 +13,7 @@ import { GuildPermission } from "@/src/types/guildTypes";
import { RequestHandler } from "express";
import { Types } from "mongoose";
import { ExportDojoRecipes, ExportResources } from "warframe-public-export-plus";
import { config } from "@/src/services/configService";
export const placeDecoInComponentController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
@ -36,6 +37,7 @@ export const placeDecoInComponentController: RequestHandler = async (req, res) =
const deco = component.Decos.find(x => x._id.equals(request.MoveId))!;
deco.Pos = request.Pos;
deco.Rot = request.Rot;
deco.Scale = request.Scale;
} else {
const deco =
component.Decos[
@ -44,6 +46,7 @@ export const placeDecoInComponentController: RequestHandler = async (req, res) =
Type: request.Type,
Pos: request.Pos,
Rot: request.Rot,
Scale: request.Scale,
Name: request.Name,
Sockets: request.Sockets
}) - 1
@ -62,37 +65,42 @@ export const placeDecoInComponentController: RequestHandler = async (req, res) =
guild.VaultShipDecorations!.find(x => x.ItemType == itemType)!.ItemCount -= 1;
}
}
if (!meta || (meta.price == 0 && meta.ingredients.length == 0)) {
deco.CompletionTime = new Date();
} 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 -= meta.price;
deco.RegularCredits = 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);
if (deco.Type != "/Lotus/Objects/Tenno/Props/TnoPaintBotDojoDeco") {
if (!meta || (meta.price == 0 && meta.ingredients.length == 0) || config.noDojoDecoBuildStage) {
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);
}
}
}
}
}
@ -107,9 +115,9 @@ interface IPlaceDecoInComponentRequest {
Type: string;
Pos: number[];
Rot: number[];
Scale?: number;
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

@ -8,7 +8,11 @@ export const releasePetController: RequestHandler = async (req, res) => {
const inventory = await getInventory(accountId, "RegularCredits KubrowPets");
const payload = getJSONfromString<IReleasePetRequest>(String(req.body));
const inventoryChanges = updateCurrency(inventory, 25000, false);
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 });
@ -18,6 +22,6 @@ export const releasePetController: RequestHandler = async (req, res) => {
};
interface IReleasePetRequest {
recipeName: "/Lotus/Types/Game/KubrowPet/ReleasePetRecipe";
recipeName: "/Lotus/Types/Game/KubrowPet/ReleasePetRecipe" | "webui";
petId: string;
}

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,53 +1,41 @@
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 { ICompletedDialogue } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger";
import { ICompletedDialogue, IDialogueDatabase } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { RequestHandler } from "express";
export const saveDialogueController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const request = JSON.parse(String(req.body)) as SaveDialogueRequest;
if ("YearIteration" in request) {
const inventory = await getInventory(accountId);
if (inventory.DialogueHistory) {
inventory.DialogueHistory.YearIteration = request.YearIteration;
} else {
inventory.DialogueHistory = { YearIteration: request.YearIteration };
}
const inventory = await getInventory(accountId, "DialogueHistory");
inventory.DialogueHistory ??= {};
inventory.DialogueHistory.YearIteration = request.YearIteration;
await inventory.save();
res.end();
} else {
const inventory = await getInventory(accountId);
if (!inventory.DialogueHistory) {
throw new Error("bad inventory state");
}
if (request.QueuedDialogues.length != 0 || request.OtherDialogueInfos.length != 0) {
logger.error(`saveDialogue request not fully handled: ${String(req.body)}`);
}
const inventoryChanges: IInventoryChanges = {};
const tomorrowAt0Utc = config.noKimCooldowns
? Date.now()
: (Math.trunc(Date.now() / 86400_000) + 1) * 86400_000;
inventory.DialogueHistory ??= {};
inventory.DialogueHistory.Dialogues ??= [];
let dialogue = inventory.DialogueHistory.Dialogues.find(x => x.DialogueName == 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
];
}
const dialogue = getDialogue(inventory, request.DialogueName);
dialogue.Rank = request.Rank;
dialogue.Chemistry = request.Chemistry;
//dialogue.QueuedDialogues = request.QueuedDialogues;
dialogue.QueuedDialogues = request.QueuedDialogues;
for (const bool of request.Booleans) {
dialogue.Booleans.push(bool);
if (bool == "LizzieShawzin") {
await addEmailItem(
inventory,
"/Lotus/Types/Items/EmailItems/LizzieShawzinSkinEmailItem",
inventoryChanges
);
}
}
for (const bool of request.ResetBooleans) {
const index = dialogue.Booleans.findIndex(x => x == bool);
@ -55,14 +43,38 @@ export const saveDialogueController: RequestHandler = async (req, res) => {
dialogue.Booleans.splice(index, 1);
}
}
dialogue.Completed.push(request.Data);
const tomorrowAt0Utc = (Math.trunc(Date.now() / (86400 * 1000)) + 1) * 86400 * 1000;
dialogue.AvailableDate = new Date(tomorrowAt0Utc);
await inventory.save();
res.json({
InventoryChanges: [],
AvailableDate: { $date: { $numberLong: tomorrowAt0Utc.toString() } }
});
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
}
if (request.Data) {
dialogue.Completed.push(request.Data);
dialogue.AvailableDate = new Date(tomorrowAt0Utc);
await inventory.save();
res.json({
InventoryChanges: inventoryChanges,
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 {
res.end();
}
}
};
@ -77,11 +89,17 @@ interface SaveCompletedDialogueRequest {
Rank: number;
Chemistry: number;
CompletionType: number;
QueuedDialogues: string[]; // unsure
QueuedDialogues: string[];
Gift?: {
Item: string;
GainedChemistry: number;
Cost: number;
GiftedQuantity: number;
};
Booleans: string[];
ResetBooleans: string[];
Data: ICompletedDialogue;
OtherDialogueInfos: IOtherDialogueInfo[]; // unsure
Data?: ICompletedDialogue;
OtherDialogueInfos: IOtherDialogueInfo[];
}
interface IOtherDialogueInfo {
@ -89,3 +107,26 @@ interface IOtherDialogueInfo {
Tag: string;
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

@ -6,14 +6,20 @@ import {
addRecipes,
addMiscItems,
addConsumables,
freeUpSlot
freeUpSlot,
combineInventoryChanges,
addCrewShipRawSalvage,
addFusionPoints
} from "@/src/services/inventoryService";
import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { ExportDojoRecipes } from "warframe-public-export-plus";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
export const sellController: RequestHandler = async (req, res) => {
const payload = JSON.parse(String(req.body)) as ISellRequest;
const accountId = await getAccountIdForRequest(req);
const requiredFields = new Set();
const requiredFields = new Set<keyof TInventoryDatabaseDocument>();
if (payload.SellCurrency == "SC_RegularCredits") {
requiredFields.add("RegularCredits");
} else if (payload.SellCurrency == "SC_FusionPoints") {
@ -22,7 +28,7 @@ export const sellController: RequestHandler = async (req, res) => {
requiredFields.add("MiscItems");
}
for (const key of Object.keys(payload.Items)) {
requiredFields.add(key);
requiredFields.add(key as keyof TInventoryDatabaseDocument);
}
if (requiredFields.has("Upgrades")) {
requiredFields.add("RawUpgrades");
@ -39,7 +45,7 @@ export const sellController: RequestHandler = async (req, res) => {
if (payload.Items.SpaceGuns || payload.Items.SpaceMelee) {
requiredFields.add(InventorySlot.SPACEWEAPONS);
}
if (payload.Items.Sentinels || payload.Items.SentinelWeapons) {
if (payload.Items.Sentinels || payload.Items.SentinelWeapons || payload.Items.MoaPets) {
requiredFields.add(InventorySlot.SENTINELS);
}
if (payload.Items.OperatorAmps) {
@ -48,13 +54,23 @@ export const sellController: RequestHandler = async (req, res) => {
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
if (payload.SellCurrency == "SC_RegularCredits") {
inventory.RegularCredits += payload.SellPrice;
} else if (payload.SellCurrency == "SC_FusionPoints") {
inventory.FusionPoints += payload.SellPrice;
addFusionPoints(inventory, payload.SellPrice);
} else if (payload.SellCurrency == "SC_PrimeBucks") {
addMiscItems(inventory, [
{
@ -69,10 +85,14 @@ export const sellController: RequestHandler = async (req, res) => {
ItemCount: payload.SellPrice
}
]);
} else if (payload.SellCurrency == "SC_Resources") {
// Will add appropriate MiscItems from CrewShipWeapons or CrewShipWeaponSkins
} else {
throw new Error("Unknown SellCurrency: " + payload.SellCurrency);
}
const inventoryChanges: IInventoryChanges = {};
// Remove item(s)
if (payload.Items.Suits) {
payload.Items.Suits.forEach(sellItem => {
@ -128,6 +148,12 @@ export const sellController: RequestHandler = async (req, res) => {
freeUpSlot(inventory, InventorySlot.SENTINELS);
});
}
if (payload.Items.MoaPets) {
payload.Items.MoaPets.forEach(sellItem => {
inventory.MoaPets.pull({ _id: sellItem.String });
freeUpSlot(inventory, InventorySlot.SENTINELS);
});
}
if (payload.Items.OperatorAmps) {
payload.Items.OperatorAmps.forEach(sellItem => {
inventory.OperatorAmps.pull({ _id: sellItem.String });
@ -145,6 +171,56 @@ export const sellController: RequestHandler = async (req, res) => {
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) {
const consumablesChanges = [];
for (const sellItem of payload.Items.Consumables) {
@ -191,7 +267,9 @@ export const sellController: RequestHandler = async (req, res) => {
}
await inventory.save();
res.json({});
res.json({
inventoryChanges: inventoryChanges // "inventoryChanges" for this response instead of the usual "InventoryChanges"
});
};
interface ISellRequest {
@ -209,9 +287,12 @@ interface ISellRequest {
SpaceMelee?: ISellItem[];
Sentinels?: ISellItem[];
SentinelWeapons?: ISellItem[];
MoaPets?: ISellItem[];
OperatorAmps?: ISellItem[];
Hoverboards?: ISellItem[];
Drones?: ISellItem[];
CrewShipWeapons?: ISellItem[];
CrewShipWeaponSkins?: ISellItem[];
};
SellPrice: number;
SellCurrency:
@ -228,3 +309,33 @@ interface ISellItem {
String: string; // oid or uniqueName
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,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

@ -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 { ISetPlacedDecoInfoRequest } from "@/src/types/shipTypes";
import { IPictureFrameInfo, ISetPlacedDecoInfoRequest } from "@/src/types/shipTypes";
import { RequestHandler } from "express";
import { handleSetPlacedDecoInfo } from "@/src/services/shipCustomizationsService";
@ -7,5 +7,17 @@ export const setPlacedDecoInfoController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const payload = JSON.parse(req.body as string) as ISetPlacedDecoInfoRequest;
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 { IOid } from "@/src/types/commonTypes";
import { Types } from "mongoose";
import { IFavouriteLoadoutDatabase, TBootLocation } from "@/src/types/shipTypes";
export const setShipFavouriteLoadoutController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const personalRooms = await getPersonalRooms(accountId);
const body = JSON.parse(String(req.body)) as ISetShipFavouriteLoadoutRequest;
if (body.BootLocation != "SHOP") {
if (body.BootLocation == "LISET") {
personalRooms.Ship.FavouriteLoadoutId = new Types.ObjectId(body.FavouriteLoadoutId.$oid);
} else if (body.BootLocation == "APARTMENT") {
updateTaggedDisplay(personalRooms.Apartment.FavouriteLoadouts, body);
} else if (body.BootLocation == "SHOP") {
updateTaggedDisplay(personalRooms.TailorShop.FavouriteLoadouts, body);
} else {
console.log(body);
throw new Error(`unexpected BootLocation: ${body.BootLocation}`);
}
const display = personalRooms.TailorShop.FavouriteLoadouts.find(x => x.Tag == body.TagName);
if (display) {
display.LoadoutId = new Types.ObjectId(body.FavouriteLoadoutId.$oid);
} else {
personalRooms.TailorShop.FavouriteLoadouts.push({
Tag: body.TagName,
LoadoutId: new Types.ObjectId(body.FavouriteLoadoutId.$oid)
});
}
await personalRooms.save();
res.json({});
res.json(body);
};
interface ISetShipFavouriteLoadoutRequest {
BootLocation: string;
BootLocation: TBootLocation;
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

@ -3,15 +3,12 @@ import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { ExportNightwave, ExportSyndicates, ISyndicateSacrifice } from "warframe-public-export-plus";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import {
addItem,
addMiscItems,
combineInventoryChanges,
getInventory,
updateCurrency
} from "@/src/services/inventoryService";
import { addMiscItems, combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { fromStoreItem, isStoreItem } from "@/src/services/itemDataService";
import { isStoreItem, toStoreItem } from "@/src/services/itemDataService";
import { logger } from "@/src/utils/logger";
const nightwaveCredsItemType = ExportNightwave.rewards[ExportNightwave.rewards.length - 1].uniqueName;
export const syndicateSacrificeController: RequestHandler = async (request, response) => {
const accountId = await getAccountIdForRequest(request);
@ -57,7 +54,7 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp
syndicate.Title ??= 0;
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 ??= [];
if (!syndicate.FreeFavorsEarned.includes(syndicate.Title)) {
syndicate.FreeFavorsEarned.push(syndicate.Title);
@ -77,10 +74,17 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp
res.NewEpisodeReward = true;
const reward = ExportNightwave.rewards[index];
let rewardType = reward.uniqueName;
if (isStoreItem(rewardType)) {
rewardType = fromStoreItem(rewardType);
if (!isStoreItem(rewardType)) {
rewardType = toStoreItem(rewardType);
}
combineInventoryChanges(res.InventoryChanges, await addItem(inventory, rewardType, reward.itemCount ?? 1));
const rewardInventoryChanges = (await handleStoreItemAcquisition(rewardType, inventory, reward.itemCount))
.InventoryChanges;
if (Object.keys(rewardInventoryChanges).length == 0) {
logger.debug(`nightwave rank up reward did not seem to get added, giving 50 creds instead`);
rewardInventoryChanges.MiscItems = [{ ItemType: nightwaveCredsItemType, ItemCount: 50 }];
addMiscItems(inventory, rewardInventoryChanges.MiscItems);
}
combineInventoryChanges(res.InventoryChanges, rewardInventoryChanges);
}
}

View File

@ -1,16 +1,9 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import {
addMiscItems,
freeUpSlot,
getInventory,
getStandingLimit,
updateStandingLimit
} from "@/src/services/inventoryService";
import { addMiscItems, addStanding, freeUpSlot, getInventory } from "@/src/services/inventoryService";
import { IMiscItem, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { IOid } from "@/src/types/commonTypes";
import { ExportSyndicates, ExportWeapons } from "warframe-public-export-plus";
import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper";
import { logger } from "@/src/utils/logger";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
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 };
}
let syndicate = inventory.Affiliations.find(x => x.Tag == request.Operation.AffiliationTag);
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;
const affiliationMod = addStanding(inventory, request.Operation.AffiliationTag, gainedStanding, true);
await inventory.save();
res.json({
InventoryChanges: inventoryChanges,
AffiliationMods: [
{
Tag: request.Operation.AffiliationTag,
Standing: gainedStanding
}
]
AffiliationMods: [affiliationMod]
});
};

View File

@ -23,7 +23,7 @@ const trainingResultController: RequestHandler = async (req, res): Promise<void>
const trainingResults = getJSONfromString<ITrainingResultsRequest>(String(req.body));
const inventory = await getInventory(accountId);
const inventory = await getInventory(accountId, "TrainingDate PlayerLevel TradesRemaining");
if (trainingResults.numLevelsGained == 1) {
let time = Date.now();
@ -33,6 +33,7 @@ const trainingResultController: RequestHandler = async (req, res): Promise<void>
inventory.TrainingDate = new Date(time);
inventory.PlayerLevel += 1;
inventory.TradesRemaining += 1;
await createMessage(accountId, [
{

View File

@ -1,10 +1,8 @@
import { RequestHandler } from "express";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
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 { ExportNightwave } from "warframe-public-export-plus";
import { logger } from "@/src/utils/logger";
import { IAffiliationMods } from "@/src/types/purchaseTypes";
export const updateChallengeProgressController: RequestHandler = async (req, res) => {
@ -12,41 +10,19 @@ export const updateChallengeProgressController: RequestHandler = async (req, res
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "ChallengeProgress SeasonChallengeHistory Affiliations");
let affiliationMods: IAffiliationMods[] = [];
if (challenges.ChallengeProgress) {
addChallenges(inventory, challenges.ChallengeProgress);
affiliationMods = addChallenges(inventory, challenges.ChallengeProgress, challenges.SeasonChallengeCompletions);
}
if (challenges.SeasonChallengeHistory) {
addSeasonalChallengeHistory(inventory, challenges.SeasonChallengeHistory);
}
const affiliationMods: IAffiliationMods[] = [];
if (challenges.ChallengeProgress && challenges.SeasonChallengeCompletions) {
for (const challenge of challenges.SeasonChallengeCompletions) {
// Ignore challenges that weren't completed just now
if (!challenges.ChallengeProgress.find(x => challenge.challenge.indexOf(x.Name) != -1)) {
continue;
challenges.SeasonChallengeHistory.forEach(({ challenge, id }) => {
const itemIndex = inventory.SeasonChallengeHistory.findIndex(i => i.challenge === challenge);
if (itemIndex !== -1) {
inventory.SeasonChallengeHistory[itemIndex].id = id;
} else {
inventory.SeasonChallengeHistory.push({ challenge, id });
}
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();

View File

@ -25,7 +25,13 @@ export const upgradesController: RequestHandler = async (req, res) => {
operation.UpgradeRequirement == "/Lotus/Types/Items/MiscItems/CustomizationSlotUnlocker"
) {
updateCurrency(inventory, 10, true);
} else if (operation.OperationType != "UOT_ABILITY_OVERRIDE") {
} 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, [
{
ItemType: operation.UpgradeRequirement,

View File

@ -1,12 +1,16 @@
import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService";
import { addFusionPoints, getInventory } from "@/src/services/inventoryService";
export const addCurrencyController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const request = req.body as IAddCurrencyRequest;
inventory[request.currency] += request.delta;
const inventory = await getInventory(accountId, request.currency);
if (request.currency == "FusionPoints") {
addFusionPoints(inventory, request.delta);
} else {
inventory[request.currency] += request.delta;
}
await inventory.save();
res.end();
};

View File

@ -0,0 +1,44 @@
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
import { ExportArcanes, ExportUpgrades } from "warframe-public-export-plus";
export const addMissingMaxRankModsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId, "Upgrades");
const maxOwnedRanks: Record<string, number> = {};
for (const upgrade of inventory.Upgrades) {
const fingerprint = JSON.parse(upgrade.UpgradeFingerprint ?? "{}") as { lvl?: number };
if (fingerprint.lvl) {
maxOwnedRanks[upgrade.ItemType] ??= 0;
if (fingerprint.lvl > maxOwnedRanks[upgrade.ItemType]) {
maxOwnedRanks[upgrade.ItemType] = fingerprint.lvl;
}
}
}
for (const [uniqueName, data] of Object.entries(ExportUpgrades)) {
if (data.fusionLimit != 0 && data.type != "PARAZON" && maxOwnedRanks[uniqueName] != data.fusionLimit) {
inventory.Upgrades.push({
ItemType: uniqueName,
UpgradeFingerprint: JSON.stringify({ lvl: data.fusionLimit })
});
}
}
for (const [uniqueName, data] of Object.entries(ExportArcanes)) {
if (
data.name != "/Lotus/Language/Items/GenericCosmeticEnhancerName" &&
maxOwnedRanks[uniqueName] != data.fusionLimit
) {
inventory.Upgrades.push({
ItemType: uniqueName,
UpgradeFingerprint: JSON.stringify({ lvl: data.fusionLimit })
});
}
}
await inventory.save();
res.end();
};

View File

@ -1,98 +0,0 @@
import { getAccountIdForRequest } from "@/src/services/loginService";
import {
getInventory,
addEquipment,
occupySlot,
productCategoryToInventoryBin,
applyDefaultUpgrades
} from "@/src/services/inventoryService";
import { modularWeaponTypes } from "@/src/helpers/modularWeaponHelper";
import { getDefaultUpgrades } from "@/src/services/itemDataService";
import { IEquipmentDatabase } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { ExportWeapons } from "warframe-public-export-plus";
import { RequestHandler } from "express";
export const addModularEquipmentController: RequestHandler = async (req, res) => {
const requiredFields = new Set();
const accountId = await getAccountIdForRequest(req);
const request = req.body as IAddModularEquipmentRequest;
const category = modularWeaponTypes[request.ItemType];
const inventoryBin = productCategoryToInventoryBin(category)!;
requiredFields.add(category);
requiredFields.add(inventoryBin);
request.ModularParts.forEach(part => {
if (ExportWeapons[part].gunType) {
if (category == "LongGuns") {
request.ItemType = {
GT_RIFLE: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary",
GT_SHOTGUN: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryShotgun",
GT_BEAM: "/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimaryBeam"
}[ExportWeapons[part].gunType];
} else {
request.ItemType = {
GT_RIFLE: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondary",
GT_SHOTGUN: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryShotgun",
GT_BEAM: "/Lotus/Weapons/SolarisUnited/Secondary/LotusModularSecondaryBeam"
}[ExportWeapons[part].gunType];
}
} else if (request.ItemType == "/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetPowerSuit") {
if (part.includes("ZanukaPetPartHead")) {
request.ItemType = {
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadA":
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetAPowerSuit",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadB":
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetBPowerSuit",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadC":
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit"
}[part]!;
}
}
});
const defaultUpgrades = getDefaultUpgrades(request.ModularParts);
if (defaultUpgrades) {
requiredFields.add("RawUpgrades");
}
const defaultWeaponsMap: Record<string, string[]> = {
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetAPowerSuit": [
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetMeleeWeaponIP"
],
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetBPowerSuit": [
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetMeleeWeaponIS"
],
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetCPowerSuit": [
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetMeleeWeaponPS"
]
};
const defaultWeapons = defaultWeaponsMap[request.ItemType] as string[] | undefined;
if (defaultWeapons) {
for (const defaultWeapon of defaultWeapons) {
const category = ExportWeapons[defaultWeapon].productCategory;
requiredFields.add(category);
requiredFields.add(productCategoryToInventoryBin(category));
}
}
const inventory = await getInventory(accountId, Array.from(requiredFields).join(" "));
if (defaultWeapons) {
for (const defaultWeapon of defaultWeapons) {
const category = ExportWeapons[defaultWeapon].productCategory;
addEquipment(inventory, category, defaultWeapon);
occupySlot(inventory, productCategoryToInventoryBin(category)!, true);
}
}
const defaultOverwrites: Partial<IEquipmentDatabase> = {
Configs: applyDefaultUpgrades(inventory, defaultUpgrades)
};
addEquipment(inventory, category, request.ItemType, request.ModularParts, undefined, defaultOverwrites);
occupySlot(inventory, inventoryBin, true);
await inventory.save();
res.end();
};
interface IAddModularEquipmentRequest {
ItemType: string;
ModularParts: string[];
}

View File

@ -1,6 +1,6 @@
import { RequestHandler } from "express";
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 { Inventory } from "@/src/models/inventoryModels/inventoryModel";
import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
@ -23,6 +23,8 @@ export const deleteAccountController: RequestHandler = async (req, res) => {
await Promise.all([
Account.deleteOne({ _id: accountId }),
GuildMember.deleteMany({ accountId: accountId }),
Ignore.deleteMany({ ignorer: accountId }),
Ignore.deleteMany({ ignoree: accountId }),
Inbox.deleteMany({ ownerId: accountId }),
Inventory.deleteOne({ accountOwnerId: accountId }),
Leaderboard.deleteMany({ ownerId: accountId }),

View File

@ -3,6 +3,7 @@ import { getDict, getItemName, getString } from "@/src/services/itemDataService"
import {
ExportArcanes,
ExportAvionics,
ExportCustoms,
ExportDrones,
ExportGear,
ExportKeys,
@ -19,6 +20,7 @@ import {
TRelicQuality
} from "warframe-public-export-plus";
import archonCrystalUpgrades from "@/static/fixed_responses/webuiArchonCrystalUpgrades.json";
import allIncarnons from "@/static/fixed_responses/allIncarnonList.json";
interface ListedItem {
uniqueName: string;
@ -28,6 +30,30 @@ interface ListedItem {
badReason?: "starter" | "frivolous" | "notraw";
partType?: string;
chainLength?: number;
parazon?: boolean;
}
interface ItemLists {
archonCrystalUpgrades: Record<string, string>;
uniqueLevelCaps: Record<string, number>;
Suits: ListedItem[];
LongGuns: ListedItem[];
Melee: ListedItem[];
ModularParts: ListedItem[];
Pistols: ListedItem[];
Sentinels: ListedItem[];
SentinelWeapons: ListedItem[];
SpaceGuns: ListedItem[];
SpaceMelee: ListedItem[];
SpaceSuits: ListedItem[];
MechSuits: ListedItem[];
miscitems: ListedItem[];
Syndicates: ListedItem[];
OperatorAmps: ListedItem[];
QuestKeys: ListedItem[];
KubrowPets: ListedItem[];
EvolutionProgress: ListedItem[];
mods: ListedItem[];
}
const relicQualitySuffixes: Record<TRelicQuality, string> = {
@ -39,22 +65,28 @@ const relicQualitySuffixes: Record<TRelicQuality, string> = {
const getItemListsController: RequestHandler = (req, response) => {
const lang = getDict(typeof req.query.lang == "string" ? req.query.lang : "en");
const res: Record<string, ListedItem[]> = {};
res.Suits = [];
res.LongGuns = [];
res.Melee = [];
res.ModularParts = [];
res.Pistols = [];
res.Sentinels = [];
res.SentinelWeapons = [];
res.SpaceGuns = [];
res.SpaceMelee = [];
res.SpaceSuits = [];
res.MechSuits = [];
res.miscitems = [];
res.Syndicates = [];
res.OperatorAmps = [];
res.QuestKeys = [];
const res: ItemLists = {
archonCrystalUpgrades,
uniqueLevelCaps: ExportMisc.uniqueLevelCaps,
Suits: [],
LongGuns: [],
Melee: [],
ModularParts: [],
Pistols: [],
Sentinels: [],
SentinelWeapons: [],
SpaceGuns: [],
SpaceMelee: [],
SpaceSuits: [],
MechSuits: [],
miscitems: [],
Syndicates: [],
OperatorAmps: [],
QuestKeys: [],
KubrowPets: [],
EvolutionProgress: [],
mods: []
};
for (const [uniqueName, item] of Object.entries(ExportWarframes)) {
res[item.productCategory].push({
uniqueName,
@ -63,20 +95,23 @@ const getItemListsController: RequestHandler = (req, response) => {
});
}
for (const [uniqueName, item] of Object.entries(ExportSentinels)) {
if (item.productCategory == "Sentinels") {
if (item.productCategory == "Sentinels" || item.productCategory == "KubrowPets") {
res[item.productCategory].push({
uniqueName,
name: getString(item.name, lang)
name: getString(item.name, lang),
exalted: item.exalted
});
}
}
for (const [uniqueName, item] of Object.entries(ExportWeapons)) {
if (item.partType) {
res.ModularParts.push({
uniqueName,
name: getString(item.name, lang),
partType: item.partType
});
if (!uniqueName.startsWith("/Lotus/Types/Items/Deimos/")) {
res.ModularParts.push({
uniqueName,
name: getString(item.name, lang),
partType: item.partType
});
}
if (uniqueName.split("/")[5] != "SentTrainingAmplifier") {
res.miscitems.push({
uniqueName: uniqueName,
@ -109,12 +144,28 @@ const getItemListsController: RequestHandler = (req, response) => {
let name = getString(item.name, lang);
if ("dissectionParts" in item) {
name = getString("/Lotus/Language/Fish/FishDisplayName", lang).split("|FISH_NAME|").join(name);
if (uniqueName.indexOf("Large") != -1) {
name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeLargeAbbrev", lang));
} else if (uniqueName.indexOf("Medium") != -1) {
name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeMediumAbbrev", lang));
if (item.syndicateTag == "CetusSyndicate") {
if (uniqueName.indexOf("Large") != -1) {
name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeLargeAbbrev", lang));
} else if (uniqueName.indexOf("Medium") != -1) {
name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeMediumAbbrev", lang));
} else {
name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeSmallAbbrev", lang));
}
} else {
name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeSmallAbbrev", lang));
if (uniqueName.indexOf("Large") != -1) {
name = name
.split("|FISH_SIZE|")
.join(getString("/Lotus/Language/SolarisVenus/RobofishAgeCategoryElderAbbrev", lang));
} else if (uniqueName.indexOf("Medium") != -1) {
name = name
.split("|FISH_SIZE|")
.join(getString("/Lotus/Language/SolarisVenus/RobofishAgeCategoryMatureAbbrev", lang));
} else {
name = name
.split("|FISH_SIZE|")
.join(getString("/Lotus/Language/SolarisVenus/RobofishAgeCategoryYoungAbbrev", lang));
}
}
}
if (
@ -150,9 +201,11 @@ const getItemListsController: RequestHandler = (req, response) => {
if (!item.hidden) {
const resultName = getItemName(item.resultType);
if (resultName) {
let itemName = getString(resultName, lang);
if (item.num > 1) itemName = `${itemName} X ${item.num}`;
res.miscitems.push({
uniqueName: uniqueName,
name: recipeNameTemplate.replace("|ITEM|", getString(resultName, lang))
name: recipeNameTemplate.replace("|ITEM|", itemName)
});
}
}
@ -169,8 +222,13 @@ const getItemListsController: RequestHandler = (req, response) => {
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 = [];
for (const [uniqueName, upgrade] of Object.entries(ExportUpgrades)) {
const mod: ListedItem = {
uniqueName,
@ -184,6 +242,9 @@ const getItemListsController: RequestHandler = (req, response) => {
} else if (upgrade.upgradeEntries) {
mod.badReason = "notraw";
}
if (upgrade.type == "PARAZON") {
mod.parazon = true;
}
res.mods.push(mod);
}
for (const [uniqueName, upgrade] of Object.entries(ExportAvionics)) {
@ -218,14 +279,21 @@ const getItemListsController: RequestHandler = (req, response) => {
name: getString(key.name || "", lang),
chainLength: key.chainStages.length
});
} else if (key.name) {
res.miscitems.push({
uniqueName,
name: getString(key.name, lang)
});
}
}
for (const uniqueName of allIncarnons) {
res.EvolutionProgress.push({
uniqueName,
name: getString(getItemName(uniqueName) || "", lang)
});
}
response.json({
archonCrystalUpgrades,
uniqueLevelCaps: ExportMisc.uniqueLevelCaps,
...res
});
response.json(res);
};
export { getItemListsController };

View File

@ -1,23 +0,0 @@
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { EquipmentFeatures } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { RequestHandler } from "express";
export const gildEquipmentController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const request = req.body as IGildEquipmentRequest;
const inventory = await getInventory(accountId, request.Category);
const weapon = inventory[request.Category].id(request.ItemId);
if (weapon) {
weapon.Features ??= 0;
weapon.Features |= EquipmentFeatures.GILDED;
await inventory.save();
}
res.end();
};
type IGildEquipmentRequest = {
ItemId: string;
Category: TEquipmentKey;
};

View File

@ -35,10 +35,8 @@ export const manageQuestsController: RequestHandler = async (req, res) => {
switch (operation) {
case "completeAll": {
if (allQuestKeys.includes(questItemType)) {
for (const questKey of inventory.QuestKeys) {
await completeQuest(inventory, questKey.ItemType);
}
for (const questKey of inventory.QuestKeys) {
await completeQuest(inventory, questKey.ItemType);
}
break;
}
@ -56,15 +54,12 @@ export const manageQuestsController: RequestHandler = async (req, res) => {
break;
}
case "deleteKey": {
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;
}
inventory.QuestKeys.pull({ ItemType: questItemType });
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": {

View File

@ -0,0 +1,33 @@
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
export const setEvolutionProgressController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const inventory = await getInventory(accountId);
const payload = req.body as ISetEvolutionProgressRequest;
inventory.EvolutionProgress ??= [];
payload.forEach(element => {
const entry = inventory.EvolutionProgress!.find(entry => entry.ItemType === element.ItemType);
if (entry) {
entry.Progress = 0;
entry.Rank = element.Rank;
} else {
inventory.EvolutionProgress!.push({
Progress: 0,
Rank: element.Rank,
ItemType: element.ItemType
});
}
});
await inventory.save();
res.end();
};
type ISetEvolutionProgressRequest = {
ItemType: string;
Rank: number;
}[];

View File

@ -1,451 +1,6 @@
import { RequestHandler } from "express";
import staticWorldState from "@/static/fixed_responses/worldState/worldState.json";
import static1999FallDays from "@/static/fixed_responses/worldState/1999_fall_days.json";
import static1999SpringDays from "@/static/fixed_responses/worldState/1999_spring_days.json";
import static1999SummerDays from "@/static/fixed_responses/worldState/1999_summer_days.json";
import static1999WinterDays from "@/static/fixed_responses/worldState/1999_winter_days.json";
import { buildConfig } from "@/src/services/buildConfigService";
import { IMongoDate, IOid } from "@/src/types/commonTypes";
import { unixTimesInMs } from "@/src/constants/timeConstants";
import { config } from "@/src/services/configService";
import { CRng } from "@/src/services/rngService";
import { ExportNightwave, ExportRegions } from "warframe-public-export-plus";
const EPOCH = 1734307200 * 1000; // Monday, Dec 16, 2024 @ 00:00 UTC+0; should logically be winter in 1999 iteration 0
import { getWorldState } from "@/src/services/worldStateService";
export const worldStateController: RequestHandler = (req, res) => {
const day = Math.trunc((Date.now() - EPOCH) / 86400000);
const week = Math.trunc(day / 7);
const weekStart = EPOCH + week * 604800000;
const weekEnd = weekStart + 604800000;
const worldState: IWorldState = {
BuildLabel:
typeof req.query.buildLabel == "string"
? req.query.buildLabel.split(" ").join("+")
: buildConfig.buildLabel,
Time: config.worldState?.lockTime || Math.round(Date.now() / 1000),
Goals: [],
GlobalUpgrades: [],
LiteSorties: [],
EndlessXpChoices: [],
SeasonInfo: {
Activation: { $date: { $numberLong: "1715796000000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
AffiliationTag: "RadioLegionIntermission12Syndicate",
Season: 14,
Phase: 0,
Params: "",
ActiveChallenges: [
getSeasonDailyChallenge(day - 2),
getSeasonDailyChallenge(day - 1),
getSeasonDailyChallenge(day - 0),
getSeasonWeeklyChallenge(week, 0),
getSeasonWeeklyChallenge(week, 1),
getSeasonWeeklyHardChallenge(week, 2),
getSeasonWeeklyHardChallenge(week, 3),
{
_id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 0).toString().padStart(8, "0") },
Activation: { $date: { $numberLong: weekStart.toString() } },
Expiry: { $date: { $numberLong: weekEnd.toString() } },
Challenge:
"/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentCompleteMissions" + (week - 12)
},
{
_id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 1).toString().padStart(8, "0") },
Activation: { $date: { $numberLong: weekStart.toString() } },
Expiry: { $date: { $numberLong: weekEnd.toString() } },
Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEximus" + (week - 12)
},
{
_id: { $oid: "67e1b96e9d00cb47" + (week * 7 + 2).toString().padStart(8, "0") },
Activation: { $date: { $numberLong: weekStart.toString() } },
Expiry: { $date: { $numberLong: weekEnd.toString() } },
Challenge: "/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanentKillEnemies" + (week - 12)
}
]
},
...staticWorldState
};
if (config.worldState?.starDays) {
worldState.Goals.push({
_id: { $oid: "67a4dcce2a198564d62e1647" },
Activation: { $date: { $numberLong: "1738868400000" } },
Expiry: { $date: { $numberLong: "2000000000000" } },
Count: 0,
Goal: 0,
Success: 0,
Personal: true,
Desc: "/Lotus/Language/Events/ValentinesFortunaName",
ToolTip: "/Lotus/Language/Events/ValentinesFortunaName",
Icon: "/Lotus/Interface/Icons/WorldStatePanel/ValentinesEventIcon.png",
Tag: "FortunaValentines",
Node: "SolarisUnitedHub1"
});
}
// Elite Sanctuary Onslaught cycling every week
worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = week; // unfaithful
// Holdfast, Cavia, & Hex bounties cycling every 2.5 hours; unfaithful implementation
const bountyCycle = Math.trunc(Date.now() / 9000000);
const bountyCycleStart = bountyCycle * 9000000;
const bountyCycleEnd = bountyCycleStart + 9000000;
worldState.SyndicateMissions[worldState.SyndicateMissions.findIndex(x => x.Tag == "ZarimanSyndicate")] = {
_id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000029" },
Activation: { $date: { $numberLong: bountyCycleStart.toString() } },
Expiry: { $date: { $numberLong: bountyCycleEnd.toString() } },
Tag: "ZarimanSyndicate",
Seed: bountyCycle,
Nodes: []
};
worldState.SyndicateMissions[worldState.SyndicateMissions.findIndex(x => x.Tag == "EntratiLabSyndicate")] = {
_id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000004" },
Activation: { $date: { $numberLong: bountyCycleStart.toString() } },
Expiry: { $date: { $numberLong: bountyCycleEnd.toString() } },
Tag: "EntratiLabSyndicate",
Seed: bountyCycle,
Nodes: []
};
worldState.SyndicateMissions[worldState.SyndicateMissions.findIndex(x => x.Tag == "HexSyndicate")] = {
_id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000006" },
Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } },
Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } },
Tag: "HexSyndicate",
Seed: bountyCycle,
Nodes: []
};
if (config.worldState?.creditBoost) {
worldState.GlobalUpgrades.push({
_id: { $oid: "5b23106f283a555109666672" },
Activation: { $date: { $numberLong: "1740164400000" } },
ExpiryDate: { $date: { $numberLong: "2000000000000" } },
UpgradeType: "GAMEPLAY_MONEY_REWARD_AMOUNT",
OperationType: "MULTIPLY",
Value: 2,
LocalizeTag: "",
LocalizeDescTag: ""
});
}
if (config.worldState?.affinityBoost) {
worldState.GlobalUpgrades.push({
_id: { $oid: "5b23106f283a555109666673" },
Activation: { $date: { $numberLong: "1740164400000" } },
ExpiryDate: { $date: { $numberLong: "2000000000000" } },
UpgradeType: "GAMEPLAY_KILL_XP_AMOUNT",
OperationType: "MULTIPLY",
Value: 2,
LocalizeTag: "",
LocalizeDescTag: ""
});
}
if (config.worldState?.resourceBoost) {
worldState.GlobalUpgrades.push({
_id: { $oid: "5b23106f283a555109666674" },
Activation: { $date: { $numberLong: "1740164400000" } },
ExpiryDate: { $date: { $numberLong: "2000000000000" } },
UpgradeType: "GAMEPLAY_PICKUP_AMOUNT",
OperationType: "MULTIPLY",
Value: 2,
LocalizeTag: "",
LocalizeDescTag: ""
});
}
// Archon Hunt cycling every week
{
const boss = ["SORTIE_BOSS_AMAR", "SORTIE_BOSS_NIRA", "SORTIE_BOSS_BOREAL"][week % 3];
const showdownNode = ["SolNode99", "SolNode53", "SolNode24"][week % 3];
const systemIndex = [3, 4, 2][week % 3]; // Mars, Jupiter, Earth
const nodes: string[] = [];
for (const [key, value] of Object.entries(ExportRegions)) {
if (
value.systemIndex === systemIndex &&
value.factionIndex !== undefined &&
value.factionIndex < 2 &&
value.name.indexOf("Archwing") == -1 &&
value.missionIndex != 0 // Exclude MT_ASSASSINATION
) {
nodes.push(key);
}
}
const rng = new CRng(week);
const firstNodeIndex = rng.randomInt(0, nodes.length - 1);
const firstNode = nodes[firstNodeIndex];
nodes.splice(firstNodeIndex, 1);
worldState.LiteSorties.push({
_id: {
$oid: Math.trunc(weekStart / 1000).toString(16) + "5e23a244740a190c"
},
Activation: { $date: { $numberLong: weekStart.toString() } },
Expiry: { $date: { $numberLong: weekEnd.toString() } },
Reward: "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards",
Seed: week,
Boss: boss,
Missions: [
{
missionType: rng.randomElement([
"MT_INTEL",
"MT_MOBILE_DEFENSE",
"MT_EXTERMINATION",
"MT_SABOTAGE",
"MT_RESCUE"
]),
node: firstNode
},
{
missionType: rng.randomElement([
"MT_DEFENSE",
"MT_TERRITORY",
"MT_ARTIFACT",
"MT_EXCAVATE",
"MT_SURVIVAL"
]),
node: rng.randomElement(nodes)
},
{
missionType: "MT_ASSASSINATION",
node: showdownNode
}
]
});
}
// Circuit choices cycling every week
worldState.EndlessXpChoices.push({
Category: "EXC_NORMAL",
Choices: [
["Nidus", "Octavia", "Harrow"],
["Gara", "Khora", "Revenant"],
["Garuda", "Baruuk", "Hildryn"],
["Excalibur", "Trinity", "Ember"],
["Loki", "Mag", "Rhino"],
["Ash", "Frost", "Nyx"],
["Saryn", "Vauban", "Nova"],
["Nekros", "Valkyr", "Oberon"],
["Hydroid", "Mirage", "Limbo"],
["Mesa", "Chroma", "Atlas"],
["Ivara", "Inaros", "Titania"]
][week % 12]
});
worldState.EndlessXpChoices.push({
Category: "EXC_HARD",
Choices: [
["Boar", "Gammacor", "Angstrum", "Gorgon", "Anku"],
["Bo", "Latron", "Furis", "Furax", "Strun"],
["Lex", "Magistar", "Boltor", "Bronco", "CeramicDagger"],
["Torid", "DualToxocyst", "DualIchor", "Miter", "Atomos"],
["AckAndBrunt", "Soma", "Vasto", "NamiSolo", "Burston"],
["Zylok", "Sibear", "Dread", "Despair", "Hate"],
["Dera", "Sybaris", "Cestra", "Sicarus", "Okina"],
["Braton", "Lato", "Skana", "Paris", "Kunai"]
][week % 8]
});
// 1999 Calendar Season cycling every week + YearIteration every 4 weeks
worldState.KnownCalendarSeasons[0].Activation = { $date: { $numberLong: weekStart.toString() } };
worldState.KnownCalendarSeasons[0].Expiry = { $date: { $numberLong: weekEnd.toString() } };
worldState.KnownCalendarSeasons[0].Season = ["CST_WINTER", "CST_SPRING", "CST_SUMMER", "CST_FALL"][week % 4];
worldState.KnownCalendarSeasons[0].Days = [
static1999WinterDays,
static1999SpringDays,
static1999SummerDays,
static1999FallDays
][week % 4];
worldState.KnownCalendarSeasons[0].YearIteration = Math.trunc(week / 4);
// Sentient Anomaly cycling every 30 minutes
const halfHour = Math.trunc(Date.now() / (unixTimesInMs.hour / 2));
const tmp = {
cavabegin: "1690761600",
PurchasePlatformLockEnabled: true,
tcsn: true,
pgr: {
ts: "1732572900",
en: "CUSTOM DECALS @ ZEVILA",
fr: "DECALS CUSTOM @ ZEVILA",
it: "DECALCOMANIE PERSONALIZZATE @ ZEVILA",
de: "AUFKLEBER NACH WUNSCH @ ZEVILA",
es: "CALCOMANÍAS PERSONALIZADAS @ ZEVILA",
pt: "DECALQUES PERSONALIZADOS NA ZEVILA",
ru: "ПОЛЬЗОВАТЕЛЬСКИЕ НАКЛЕЙКИ @ ЗеВиЛа",
pl: "NOWE NAKLEJKI @ ZEVILA",
uk: "КОРИСТУВАЦЬКІ ДЕКОЛІ @ ЗІВІЛА",
tr: "ÖZEL ÇIKARTMALAR @ ZEVILA",
ja: "カスタムデカール @ ゼビラ",
zh: "定制贴花认准泽威拉",
ko: "커스텀 데칼 @ ZEVILA",
tc: "自訂貼花 @ ZEVILA",
th: "รูปลอกสั่งทำที่ ZEVILA"
},
ennnd: true,
mbrt: true,
sfn: [550, 553, 554, 555][halfHour % 4]
};
worldState.Tmp = JSON.stringify(tmp);
res.json(worldState);
};
interface IWorldState {
Version: number; // for goals
BuildLabel: string;
Time: number;
Goals: IGoal[];
SyndicateMissions: ISyndicateMission[];
GlobalUpgrades: IGlobalUpgrade[];
LiteSorties: ILiteSortie[];
NodeOverrides: INodeOverride[];
EndlessXpChoices: IEndlessXpChoice[];
SeasonInfo: {
Activation: IMongoDate;
Expiry: IMongoDate;
AffiliationTag: string;
Season: number;
Phase: number;
Params: string;
ActiveChallenges: ISeasonChallenge[];
};
KnownCalendarSeasons: ICalendarSeason[];
Tmp?: string;
}
interface IGoal {
_id: IOid;
Activation: IMongoDate;
Expiry: IMongoDate;
Count: number;
Goal: number;
Success: number;
Personal: boolean;
Desc: string;
ToolTip: string;
Icon: string;
Tag: string;
Node: string;
}
interface ISyndicateMission {
_id: IOid;
Activation: IMongoDate;
Expiry: IMongoDate;
Tag: string;
Seed: number;
Nodes: string[];
}
interface IGlobalUpgrade {
_id: IOid;
Activation: IMongoDate;
ExpiryDate: IMongoDate;
UpgradeType: string;
OperationType: string;
Value: number;
LocalizeTag: string;
LocalizeDescTag: string;
}
interface INodeOverride {
_id: IOid;
Activation?: IMongoDate;
Expiry?: IMongoDate;
Node: string;
Hide?: boolean;
Seed?: number;
LevelOverride?: string;
Faction?: string;
CustomNpcEncounters?: string;
}
interface ILiteSortie {
_id: IOid;
Activation: IMongoDate;
Expiry: IMongoDate;
Reward: "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards";
Seed: number;
Boss: string; // "SORTIE_BOSS_AMAR" | "SORTIE_BOSS_NIRA" | "SORTIE_BOSS_BOREAL"
Missions: {
missionType: string;
node: string;
}[];
}
interface IEndlessXpChoice {
Category: string;
Choices: string[];
}
interface ISeasonChallenge {
_id: IOid;
Daily?: boolean;
Activation: IMongoDate;
Expiry: IMongoDate;
Challenge: string;
}
interface ICalendarSeason {
Activation: IMongoDate;
Expiry: IMongoDate;
Season: string; // "CST_UNDEFINED" | "CST_WINTER" | "CST_SPRING" | "CST_SUMMER" | "CST_FALL"
Days: {
day: number;
}[];
YearIteration: number;
}
const dailyChallenges = Object.keys(ExportNightwave.challenges).filter(x =>
x.startsWith("/Lotus/Types/Challenges/Seasons/Daily/")
);
const getSeasonDailyChallenge = (day: number): ISeasonChallenge => {
const dayStart = EPOCH + day * 86400000;
const dayEnd = EPOCH + (day + 3) * 86400000;
const rng = new CRng(day);
return {
_id: { $oid: "67e1b5ca9d00cb47" + day.toString().padStart(8, "0") },
Daily: true,
Activation: { $date: { $numberLong: dayStart.toString() } },
Expiry: { $date: { $numberLong: dayEnd.toString() } },
Challenge: rng.randomElement(dailyChallenges)
};
};
const weeklyChallenges = Object.keys(ExportNightwave.challenges).filter(
x =>
x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/") &&
!x.startsWith("/Lotus/Types/Challenges/Seasons/Weekly/SeasonWeeklyPermanent")
);
const getSeasonWeeklyChallenge = (week: number, id: number): ISeasonChallenge => {
const weekStart = EPOCH + week * 604800000;
const weekEnd = weekStart + 604800000;
const challengeId = week * 7 + id;
const rng = new CRng(challengeId);
return {
_id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") },
Activation: { $date: { $numberLong: weekStart.toString() } },
Expiry: { $date: { $numberLong: weekEnd.toString() } },
Challenge: rng.randomElement(weeklyChallenges)
};
};
const weeklyHardChallenges = Object.keys(ExportNightwave.challenges).filter(x =>
x.startsWith("/Lotus/Types/Challenges/Seasons/WeeklyHard/")
);
const getSeasonWeeklyHardChallenge = (week: number, id: number): ISeasonChallenge => {
const weekStart = EPOCH + week * 604800000;
const weekEnd = weekStart + 604800000;
const challengeId = week * 7 + id;
const rng = new CRng(challengeId);
return {
_id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") },
Activation: { $date: { $numberLong: weekStart.toString() } },
Expiry: { $date: { $numberLong: weekEnd.toString() } },
Challenge: rng.randomElement(weeklyHardChallenges)
};
res.json(getWorldState(req.query.buildLabel as string | undefined));
};

View File

@ -27,7 +27,15 @@ const viewController: RequestHandler = async (req, res) => {
for (const type of Object.keys(ExportEnemies.avatars)) {
if (!scans.has(type)) scans.add(type);
}
responseJson.Scans ??= [];
// Take any existing scans and also set them to 9999
if (responseJson.Scans) {
for (const scan of responseJson.Scans) {
scans.add(scan.type);
}
}
responseJson.Scans = [];
for (const type of scans) {
responseJson.Scans.push({ type: type, scans: 9999 });
}

View File

@ -1,5 +1,6 @@
import { IMongoDate, IOid } from "@/src/types/commonTypes";
import { Types } from "mongoose";
import { TRarity } from "warframe-public-export-plus";
export const toOid = (objectId: Types.ObjectId): IOid => {
return { $oid: objectId.toString() } satisfies IOid;
@ -8,3 +9,144 @@ export const toOid = (objectId: Types.ObjectId): IOid => {
export const toMongoDate = (date: Date): IMongoDate => {
return { $date: { $numberLong: date.getTime().toString() } };
};
export const kubrowWeights: Record<TRarity, number> = {
COMMON: 6,
UNCOMMON: 4,
RARE: 2,
LEGENDARY: 1
};
export const kubrowFurPatternsWeights: Record<TRarity, number> = {
COMMON: 6,
UNCOMMON: 5,
RARE: 2,
LEGENDARY: 1
};
export const catbrowDetails = {
Colors: [
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseA", rarity: "COMMON" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseB", rarity: "COMMON" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseC", rarity: "COMMON" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorBaseD", rarity: "COMMON" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryA", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryB", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryC", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorSecondaryD", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryA", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryB", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryC", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorTertiaryD", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsA", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsB", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsC", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorAccentsD", rarity: "LEGENDARY" as TRarity }
],
EyeColors: [
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesA", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesB", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesC", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesD", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesE", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesF", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesG", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesH", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesI", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesJ", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesK", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesL", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesM", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Colors/CatbrowPetColorEyesN", rarity: "LEGENDARY" as TRarity }
],
FurPatterns: [{ type: "/Lotus/Types/Game/CatbrowPet/Patterns/CatbrowPetPatternA", rarity: "COMMON" as TRarity }],
BodyTypes: [
{ type: "/Lotus/Types/Game/CatbrowPet/BodyTypes/CatbrowPetRegularBodyType", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/BodyTypes/CatbrowPetRegularBodyType", rarity: "LEGENDARY" as TRarity }
],
Heads: [
{ type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadA", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadB", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadC", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Heads/CatbrowHeadD", rarity: "LEGENDARY" as TRarity }
],
Tails: [
{ type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailA", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailB", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailC", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailD", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/CatbrowPet/Tails/CatbrowTailE", rarity: "LEGENDARY" as TRarity }
]
};
export const kubrowDetails = {
Colors: [
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneA", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneB", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneC", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneD", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneE", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneF", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneG", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMundaneH", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidA", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidB", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidC", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidD", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidE", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidF", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidG", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorMidH", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantA", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantB", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantC", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantD", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantE", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantF", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantG", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorVibrantH", rarity: "LEGENDARY" as TRarity }
],
EyeColors: [
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesA", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesB", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesC", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesD", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesE", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesF", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesG", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesH", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Colors/KubrowPetColorEyesI", rarity: "LEGENDARY" as TRarity }
],
FurPatterns: [
{ type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternB", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternA", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternC", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternD", rarity: "RARE" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternE", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/Patterns/KubrowPetPatternF", rarity: "LEGENDARY" as TRarity }
],
BodyTypes: [
{ type: "/Lotus/Types/Game/KubrowPet/BodyTypes/KubrowPetRegularBodyType", rarity: "UNCOMMON" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/BodyTypes/KubrowPetHeavyBodyType", rarity: "LEGENDARY" as TRarity },
{ type: "/Lotus/Types/Game/KubrowPet/BodyTypes/KubrowPetThinBodyType", rarity: "LEGENDARY" as TRarity }
],
Heads: [],
Tails: []
};

View File

@ -1,6 +1,14 @@
import { ExportRegions } from "warframe-public-export-plus";
import { IInfNode } from "@/src/types/inventoryTypes/inventoryTypes";
import { SRng } from "@/src/services/rngService";
import { ExportRegions, ExportWarframes } from "warframe-public-export-plus";
import { IInfNode, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes";
import { getRewardAtPercentage, SRng } from "@/src/services/rngService";
import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel";
import { logger } from "../utils/logger";
import { IOid } from "../types/commonTypes";
import { Types } from "mongoose";
import { addMods, generateRewardSeed } from "../services/inventoryService";
import { isArchwingMission } from "../services/worldStateService";
import { fromStoreItem, toStoreItem } from "../services/itemDataService";
import { createMessage } from "../services/inboxService";
export const getInfNodes = (faction: string, rank: number): IInfNode[] => {
const infNodes = [];
@ -17,7 +25,7 @@ export const getInfNodes = (faction: string, rank: number): IInfNode[] => {
value.missionIndex != 42 && // not face off
value.name.indexOf("1999NodeI") == -1 && // not stage defence
value.name.indexOf("1999NodeJ") == -1 && // not lich bounty
value.name.indexOf("Archwing") == -1
!isArchwingMission(value)
) {
//console.log(dict_en[value.name]);
infNodes.push({ Node: key, Influence: 1 });
@ -32,13 +40,339 @@ const systemIndexes: Record<string, number[]> = {
FC_INFESTATION: [23]
};
export const showdownNodes: Record<string, string> = {
FC_GRINEER: "CrewBattleNode557",
FC_CORPUS: "CrewBattleNode558",
FC_INFESTATION: "CrewBattleNode559"
};
// Get a parazon 'passcode' based on the nemesis fingerprint so it's always the same for the same nemesis.
export const getNemesisPasscode = (fp: bigint, faction: string): number[] => {
const rng = new SRng(fp);
const passcode = [rng.randomInt(0, 7)];
if (faction != "FC_INFESTATION") {
passcode.push(rng.randomInt(0, 7));
passcode.push(rng.randomInt(0, 7));
export const getNemesisPasscode = (nemesis: { fp: bigint; Faction: string }): number[] => {
const rng = new SRng(nemesis.fp);
const choices = [0, 1, 2, 3, 5, 6, 7];
let choiceIndex = rng.randomInt(0, choices.length - 1);
const passcode = [choices[choiceIndex]];
if (nemesis.Faction != "FC_INFESTATION") {
choices.splice(choiceIndex, 1);
choiceIndex = rng.randomInt(0, choices.length - 1);
passcode.push(choices[choiceIndex]);
choices.splice(choiceIndex, 1);
choiceIndex = rng.randomInt(0, choices.length - 1);
passcode.push(choices[choiceIndex]);
}
return passcode;
};
const reqiuemMods: readonly 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"
];
const antivirusMods: readonly string[] = [
"/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"
];
export const getNemesisPasscodeModTypes = (nemesis: { fp: bigint; Faction: string }): string[] => {
const passcode = getNemesisPasscode(nemesis);
return nemesis.Faction == "FC_INFESTATION"
? passcode.map(i => antivirusMods[i])
: passcode.map(i => reqiuemMods[i]);
};
export const encodeNemesisGuess = (
symbol1: number,
result1: number,
symbol2: number,
result2: number,
symbol3: number,
result3: number
): number => {
return (
(symbol1 & 0xf) |
((result1 & 3) << 12) |
((symbol2 << 4) & 0xff) |
((result2 << 14) & 0xffff) |
((symbol3 & 0xf) << 8) |
((result3 & 3) << 16)
);
};
export const decodeNemesisGuess = (val: number): number[] => {
return [val & 0xf, (val >> 12) & 3, (val & 0xff) >> 4, (val & 0xffff) >> 14, (val >> 8) & 0xf, (val >> 16) & 3];
};
export interface IKnifeResponse {
UpgradeIds?: string[];
UpgradeTypes?: string[];
UpgradeFingerprints?: { lvl: number }[];
UpgradeNew?: boolean[];
HasKnife?: boolean;
}
export const getKnifeUpgrade = (
inventory: TInventoryDatabaseDocument,
dataknifeUpgrades: string[],
type: string
): { ItemId: IOid; ItemType: string } => {
if (dataknifeUpgrades.indexOf(type) != -1) {
return {
ItemId: { $oid: "000000000000000000000000" },
ItemType: type
};
}
for (const upgradeId of dataknifeUpgrades) {
if (upgradeId.length == 24) {
const upgrade = inventory.Upgrades.id(upgradeId);
if (upgrade && upgrade.ItemType == type) {
return {
ItemId: { $oid: upgradeId },
ItemType: type
};
}
}
}
throw new Error(`${type} does not seem to be installed on parazon?!`);
};
export const consumeModCharge = (
response: IKnifeResponse,
inventory: TInventoryDatabaseDocument,
upgrade: { ItemId: IOid; ItemType: string },
dataknifeUpgrades: string[]
): void => {
response.UpgradeIds ??= [];
response.UpgradeTypes ??= [];
response.UpgradeFingerprints ??= [];
response.UpgradeNew ??= [];
response.HasKnife = true;
if (upgrade.ItemId.$oid != "000000000000000000000000") {
const dbUpgrade = inventory.Upgrades.id(upgrade.ItemId.$oid)!;
const fingerprint = JSON.parse(dbUpgrade.UpgradeFingerprint!) as { lvl: number };
fingerprint.lvl += 1;
dbUpgrade.UpgradeFingerprint = JSON.stringify(fingerprint);
response.UpgradeIds.push(upgrade.ItemId.$oid);
response.UpgradeTypes.push(upgrade.ItemType);
response.UpgradeFingerprints.push(fingerprint);
response.UpgradeNew.push(false);
} else {
const id = new Types.ObjectId();
inventory.Upgrades.push({
_id: id,
ItemType: upgrade.ItemType,
UpgradeFingerprint: `{"lvl":1}`
});
addMods(inventory, [
{
ItemType: upgrade.ItemType,
ItemCount: -1
}
]);
const dataknifeRawUpgradeIndex = dataknifeUpgrades.indexOf(upgrade.ItemType);
if (dataknifeRawUpgradeIndex != -1) {
dataknifeUpgrades[dataknifeRawUpgradeIndex] = id.toString();
} else {
logger.warn(`${upgrade.ItemType} not found in dataknife config`);
}
response.UpgradeIds.push(id.toString());
response.UpgradeTypes.push(upgrade.ItemType);
response.UpgradeFingerprints.push({ lvl: 1 });
response.UpgradeNew.push(true);
}
};
const kuvaLichVersionSixWeapons = [
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Drakgoon/KuvaDrakgoon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Karak/KuvaKarak",
"/Lotus/Weapons/Grineer/Melee/GrnKuvaLichScythe/GrnKuvaLichScytheWeapon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Kohm/KuvaKohm",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Ogris/KuvaOgris",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Quartakk/KuvaQuartakk",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Tonkor/KuvaTonkor",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Brakk/KuvaBrakk",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Kraken/KuvaKraken",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Seer/KuvaSeer",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Stubba/KuvaStubba",
"/Lotus/Weapons/Grineer/HeavyWeapons/GrnHeavyGrenadeLauncher",
"/Lotus/Weapons/Grineer/LongGuns/GrnKuvaLichRifle/GrnKuvaLichRifleWeapon",
"/Lotus/Weapons/Grineer/Bows/GrnBow/GrnBowWeapon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hind/KuvaHind",
"/Lotus/Weapons/Grineer/KuvaLich/Secondaries/Nukor/KuvaNukor",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Hek/KuvaHekWeapon",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Zarr/KuvaZarr",
"/Lotus/Weapons/Grineer/KuvaLich/HeavyWeapons/Grattler/KuvaGrattler",
"/Lotus/Weapons/Grineer/KuvaLich/LongGuns/Sobek/KuvaSobek"
];
const corpusVersionThreeWeapons = [
"/Lotus/Weapons/Corpus/LongGuns/CrpBriefcaseLauncher/CrpBriefcaseLauncher",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEArcaPlasmor/CrpBEArcaPlasmor",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEFluxRifle/CrpBEFluxRifle",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBETetra/CrpBETetra",
"/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBECycron/CrpBECycron",
"/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEDetron/CrpBEDetron",
"/Lotus/Weapons/Corpus/Pistols/CrpIgniterPistol/CrpIgniterPistol",
"/Lotus/Weapons/Corpus/Pistols/CrpBriefcaseAkimbo/CrpBriefcaseAkimboPistol",
"/Lotus/Weapons/Corpus/BoardExec/Secondary/CrpBEPlinx/CrpBEPlinxWeapon",
"/Lotus/Weapons/Corpus/BoardExec/Primary/CrpBEGlaxion/CrpBEGlaxion"
];
export const getWeaponsForManifest = (manifest: string): readonly string[] => {
switch (manifest) {
case "/Lotus/Types/Game/Nemesis/KuvaLich/KuvaLichManifestVersionSix":
return kuvaLichVersionSixWeapons;
case "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionThree":
case "/Lotus/Types/Enemies/Corpus/Lawyers/LawyerManifestVersionFour":
return corpusVersionThreeWeapons;
}
throw new Error(`unknown nemesis manifest: ${manifest}`);
};
export const getInnateDamageTag = (
KillingSuit: string
):
| "InnateElectricityDamage"
| "InnateFreezeDamage"
| "InnateHeatDamage"
| "InnateImpactDamage"
| "InnateMagDamage"
| "InnateRadDamage"
| "InnateToxinDamage" => {
return ExportWarframes[KillingSuit].nemesisUpgradeTag!;
};
// TODO: For -1399275245665749231n, the value should be 75306944, but we're off by 59 with 75307003.
export const getInnateDamageValue = (fp: bigint): number => {
const rng = new SRng(fp);
rng.randomFloat(); // used for the weapon index
const WeaponUpgradeValueAttenuationExponent = 2.25;
let value = Math.pow(rng.randomFloat(), WeaponUpgradeValueAttenuationExponent);
if (value >= 0.941428) {
value = 1;
}
return Math.trunc(value * 0x40000000);
};
export const getKillTokenRewardCount = (fp: bigint): number => {
const rng = new SRng(fp);
return rng.randomInt(10, 15);
};
// /Lotus/Types/Enemies/InfestedLich/InfestedLichRewardManifest
const infestedLichRotA = [
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyDJRomHuman", probability: 0.046 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyDJRomInfested", probability: 0.045 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyDrillbitHuman", probability: 0.046 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyDrillbitInfested", probability: 0.045 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyHarddriveHuman", probability: 0.046 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyHarddriveInfested", probability: 0.045 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyPacketHuman", probability: 0.046 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyPacketInfested", probability: 0.045 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyZekeHuman", probability: 0.046 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/Plushies/PlushyZekeInfested", probability: 0.045 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandBillboardPosterA", probability: 0.045 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandBillboardPosterB", probability: 0.046 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandDespairPoster", probability: 0.045 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandGridPoster", probability: 0.046 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandHuddlePoster", probability: 0.045 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandJumpPoster", probability: 0.046 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandLimoPoster", probability: 0.045 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandLookingDownPosterDay", probability: 0.046 },
{
type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandLookingDownPosterNight",
probability: 0.045
},
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandSillyPoster", probability: 0.046 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandWhiteBluePoster", probability: 0.045 },
{ type: "/Lotus/StoreItems/Types/Items/ShipDecos/BoybandPosters/BoybandWhitePinkPoster", probability: 0.045 }
];
const infestedLichRotB = [
{ type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraA", probability: 0.072 },
{ type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraB", probability: 0.071 },
{ type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraC", probability: 0.072 },
{ type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraD", probability: 0.071 },
{ type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraE", probability: 0.072 },
{ type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraF", probability: 0.071 },
{ type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraG", probability: 0.071 },
{ type: "/Lotus/StoreItems/Upgrades/Skins/Effects/InfestedLichEphemeraH", probability: 0.072 },
{ type: "/Lotus/StoreItems/Types/Items/Emotes/DanceDJRomHype", probability: 0.071 },
{ type: "/Lotus/StoreItems/Types/Items/Emotes/DancePacketWindmillShuffle", probability: 0.072 },
{ type: "/Lotus/StoreItems/Types/Items/Emotes/DanceHarddrivePony", probability: 0.071 },
{ type: "/Lotus/StoreItems/Types/Items/Emotes/DanceDrillbitCrisscross", probability: 0.072 },
{ type: "/Lotus/StoreItems/Types/Items/Emotes/DanceZekeCanthavethis", probability: 0.071 },
{ type: "/Lotus/StoreItems/Types/Items/PhotoBooth/PhotoboothTileRJLasXStadiumBossArena", probability: 0.071 }
];
export const getInfestedLichItemRewards = (fp: bigint): string[] => {
const rng = new SRng(fp);
const rotAReward = getRewardAtPercentage(infestedLichRotA, rng.randomFloat())!.type;
rng.randomFloat(); // unused afaict
const rotBReward = getRewardAtPercentage(infestedLichRotB, rng.randomFloat())!.type;
return [rotAReward, rotBReward];
};
export const sendCodaFinishedMessage = async (
inventory: TInventoryDatabaseDocument,
fp: bigint = generateRewardSeed(),
name: string = "ZEKE_BEATWOMAN_TM.1999",
killed: boolean = true
): Promise<void> => {
const att: string[] = [];
// First vanquish/convert gives a sigil
const sigil = killed
? "/Lotus/Upgrades/Skins/Sigils/InfLichVanquishedSigil"
: "/Lotus/Upgrades/Skins/Sigils/InfLichConvertedSigil";
if (!inventory.WeaponSkins.find(x => x.ItemType == sigil)) {
att.push(toStoreItem(sigil));
}
const [rotAReward, rotBReward] = getInfestedLichItemRewards(fp);
att.push(fromStoreItem(rotAReward));
att.push(fromStoreItem(rotBReward));
let countedAtt: ITypeCount[] | undefined;
if (killed) {
countedAtt = [
{
ItemType: "/Lotus/Types/Items/MiscItems/CodaWeaponBucks",
ItemCount: getKillTokenRewardCount(fp)
}
];
}
await createMessage(inventory.accountOwnerId, [
{
sndr: "/Lotus/Language/Bosses/Ordis",
msg: "/Lotus/Language/Inbox/VanquishBandMsgBody",
arg: [
{
Key: "LICH_NAME",
Tag: name
}
],
att: att,
countedAtt: countedAtt,
sub: "/Lotus/Language/Inbox/VanquishBandMsgTitle",
icon: "/Lotus/Interface/Icons/Npcs/Ordis.png",
highPriority: true
}
]);
};

View File

@ -31,7 +31,7 @@ export interface IFingerprintStat {
}
export const createVeiledRivenFingerprint = (meta: IUpgrade): IVeiledRivenFingerprint => {
const challenge = getRandomElement(meta.availableChallenges!);
const challenge = getRandomElement(meta.availableChallenges!)!;
const fingerprintChallenge: IRivenChallenge = {
Type: challenge.fullName,
Progress: 0,
@ -54,11 +54,11 @@ export const createVeiledRivenFingerprint = (meta: IUpgrade): IVeiledRivenFinger
export const createUnveiledRivenFingerprint = (meta: IUpgrade): IUnveiledRivenFingerprint => {
const fingerprint: IUnveiledRivenFingerprint = {
compat: getRandomElement(meta.compatibleItems!),
compat: getRandomElement(meta.compatibleItems!)!,
lim: 0,
lvl: 0,
lvlReq: getRandomInt(8, 16),
pol: getRandomElement(["AP_ATTACK", "AP_DEFENSE", "AP_TACTIC"]),
pol: getRandomElement(["AP_ATTACK", "AP_DEFENSE", "AP_TACTIC"])!,
buffs: [],
curses: []
};
@ -81,7 +81,7 @@ export const randomiseRivenStats = (meta: IUpgrade, fingerprint: IUnveiledRivenF
if (Math.random() < 0.5) {
const entry = getRandomElement(
meta.upgradeEntries!.filter(x => x.canBeCurse && !fingerprint.buffs.find(y => y.Tag == x.tag))
);
)!;
fingerprint.curses.push({ Tag: entry.tag, Value: Math.trunc(Math.random() * 0x40000000) });
}
};

View File

@ -23,13 +23,15 @@ const dojoDecoSchema = new Schema<IDojoDecoDatabase>({
Type: String,
Pos: [Number],
Rot: [Number],
Scale: Number,
Name: String,
Sockets: Number,
RegularCredits: Number,
MiscItems: { type: [typeCountSchema], default: undefined },
CompletionTime: Date,
RushPlatinum: Number,
PictureFrameInfo: pictureFrameInfoSchema
PictureFrameInfo: pictureFrameInfoSchema,
Pending: Boolean
});
const dojoLeaderboardEntrySchema = new Schema<IDojoLeaderboardEntry>(
@ -42,6 +44,7 @@ const dojoLeaderboardEntrySchema = new Schema<IDojoLeaderboardEntry>(
);
const dojoComponentSchema = new Schema<IDojoComponentDatabase>({
SortId: Schema.Types.ObjectId,
pf: { type: String, required: true },
ppf: String,
pi: Schema.Types.ObjectId,
@ -57,6 +60,12 @@ const dojoComponentSchema = new Schema<IDojoComponentDatabase>({
DestructionTime: Date,
Decos: [dojoDecoSchema],
DecoCapacity: Number,
PaintBot: Schema.Types.ObjectId,
PendingColors: { type: [Number], default: undefined },
Colors: { type: [Number], default: undefined },
PendingLights: { type: [Number], default: undefined },
Lights: { type: [Number], default: undefined },
Settings: String,
Leaderboard: { type: [dojoLeaderboardEntrySchema], default: undefined }
});
@ -183,6 +192,7 @@ const guildSchema = new Schema<IGuildDatabase>(
VaultMiscItems: { type: [typeCountSchema], default: undefined },
VaultShipDecorations: { type: [typeCountSchema], default: undefined },
VaultFusionTreasures: { type: [fusionTreasuresSchema], default: undefined },
VaultDecoRecipes: { type: [typeCountSchema], default: undefined },
TechProjects: { type: [techProjectSchema], default: undefined },
ActiveDojoColorResearch: { type: String, default: "" },
Class: { type: Number, default: 0 },

View File

@ -4,7 +4,8 @@ import { typeCountSchema } from "@/src/models/inventoryModels/inventoryModel";
import { IMongoDate, IOid } from "@/src/types/commonTypes";
import { ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes";
export interface IMessageClient extends Omit<IMessageDatabase, "_id" | "date" | "startDate" | "endDate" | "ownerId"> {
export interface IMessageClient
extends Omit<IMessageDatabase, "_id" | "date" | "startDate" | "endDate" | "ownerId" | "attVisualOnly" | "expiry"> {
_id?: IOid;
date: IMongoDate;
startDate?: IMongoDate;
@ -15,6 +16,8 @@ export interface IMessageClient extends Omit<IMessageDatabase, "_id" | "date" |
export interface IMessageDatabase extends IMessage {
ownerId: Types.ObjectId;
date: Date; //created at
attVisualOnly?: boolean;
expiry?: Date;
_id: Types.ObjectId;
}
@ -108,6 +111,7 @@ const messageSchema = new Schema<IMessageDatabase>(
att: { type: [String], default: undefined },
gifts: { type: [giftSchema], default: undefined },
countedAtt: { type: [typeCountSchema], default: undefined },
attVisualOnly: Boolean,
transmission: String,
arg: {
type: [
@ -134,13 +138,14 @@ messageSchema.virtual("messageId").get(function (this: IMessageDatabase) {
messageSchema.set("toJSON", {
virtuals: true,
transform(_document, returnedObject) {
delete returnedObject.ownerId;
const messageDatabase = returnedObject as IMessageDatabase;
const messageClient = returnedObject as IMessageClient;
delete returnedObject._id;
delete returnedObject.__v;
delete returnedObject.ownerId;
delete returnedObject.attVisualOnly;
delete returnedObject.expiry;
messageClient.date = toMongoDate(messageDatabase.date);
@ -153,5 +158,6 @@ messageSchema.set("toJSON", {
});
messageSchema.index({ ownerId: 1 });
messageSchema.index({ expiry: 1 }, { expireAfterSeconds: 0 });
export const Inbox = model<IMessageDatabase>("Inbox", messageSchema, "inbox");

View File

@ -39,10 +39,9 @@ import {
ILoreFragmentScan,
IEvolutionProgress,
IEndlessXpProgress,
ICrewShipPortGuns,
ICrewShipCustomization,
ICrewShipWeapon,
ICrewShipPilotWeapon,
ICrewShipWeaponEmplacements,
IShipExterior,
IHelminthFoodRecord,
ICrewShipMembersDatabase,
@ -84,7 +83,21 @@ import {
IInfNode,
IDiscoveredMarker,
IWeeklyMission,
ILockedWeaponGroupDatabase
ILockedWeaponGroupDatabase,
IPersonalTechProjectDatabase,
IPersonalTechProjectClient,
ILastSortieRewardDatabase,
ILastSortieRewardClient,
ICrewMemberSkill,
ICrewMemberSkillEfficiency,
ICrewMemberDatabase,
ICrewMemberClient,
ISortieRewardAttenuation,
IInvasionProgressDatabase,
IInvasionProgressClient,
IAccolades,
IHubNpcCustomization,
ILotusCustomization
} from "../../types/inventoryTypes/inventoryTypes";
import { IOid } from "../../types/commonTypes";
import {
@ -98,7 +111,7 @@ import {
IEquipmentClient
} from "@/src/types/inventoryTypes/commonInventoryTypes";
import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers";
import { EquipmentSelectionSchema } from "./loadoutModel";
import { EquipmentSelectionSchema, oidSchema } from "./loadoutModel";
export const typeCountSchema = new Schema<ITypeCount>({ ItemType: String, ItemCount: Number }, { _id: false });
@ -290,6 +303,55 @@ upgradeSchema.set("toJSON", {
}
});
const crewMemberSkillSchema = new Schema<ICrewMemberSkill>(
{
Assigned: Number
},
{ _id: false }
);
const crewMemberSkillEfficiencySchema = new Schema<ICrewMemberSkillEfficiency>(
{
PILOTING: crewMemberSkillSchema,
GUNNERY: crewMemberSkillSchema,
ENGINEERING: crewMemberSkillSchema,
COMBAT: crewMemberSkillSchema,
SURVIVABILITY: crewMemberSkillSchema
},
{ _id: false }
);
const crewMemberSchema = new Schema<ICrewMemberDatabase>(
{
ItemType: { type: String, required: true },
NemesisFingerprint: { type: BigInt, default: 0n },
Seed: { type: BigInt, default: 0n },
AssignedRole: Number,
SkillEfficiency: crewMemberSkillEfficiencySchema,
WeaponConfigIdx: Number,
WeaponId: { type: Schema.Types.ObjectId, default: "000000000000000000000000" },
XP: { type: Number, default: 0 },
PowersuitType: { type: String, required: true },
Configs: [ItemConfigSchema],
SecondInCommand: { type: Boolean, default: false }
},
{ id: false }
);
crewMemberSchema.set("toJSON", {
virtuals: true,
transform(_doc, obj) {
const db = obj as ICrewMemberDatabase;
const client = obj as ICrewMemberClient;
client.WeaponId = toOid(db.WeaponId);
client.ItemId = toOid(db._id);
delete obj._id;
delete obj.__v;
}
});
const slotsBinSchema = new Schema<ISlots>(
{
Slots: Number,
@ -329,7 +391,7 @@ MailboxSchema.set("toJSON", {
const DuviriInfoSchema = new Schema<IDuviriInfo>(
{
Seed: Number,
Seed: BigInt,
NumCompletions: { type: Number, default: 0 }
},
{
@ -498,7 +560,39 @@ const seasonChallengeHistorySchema = new Schema<ISeasonChallenge>(
{ _id: false }
);
//TODO: check whether this is complete
const personalTechProjectSchema = new Schema<IPersonalTechProjectDatabase>({
State: Number,
ReqCredits: Number,
ItemType: String,
ProductCategory: String,
CategoryItemId: Schema.Types.ObjectId,
ReqItems: { type: [typeCountSchema], default: undefined },
HasContributions: Boolean,
CompletionDate: Date
});
personalTechProjectSchema.virtual("ItemId").get(function () {
return { $oid: this._id.toString() };
});
personalTechProjectSchema.set("toJSON", {
virtuals: true,
transform(_doc, ret, _options) {
delete ret._id;
delete ret.__v;
const db = ret as IPersonalTechProjectDatabase;
const client = ret as IPersonalTechProjectClient;
if (db.CategoryItemId) {
client.CategoryItemId = toOid(db.CategoryItemId);
}
if (db.CompletionDate) {
client.CompletionDate = toMongoDate(db.CompletionDate);
}
}
});
const playerSkillsSchema = new Schema<IPlayerSkills>(
{
LPP_SPACE: { type: Number, default: 0 },
@ -595,6 +689,27 @@ questKeysSchema.set("toJSON", {
export const fusionTreasuresSchema = new Schema<IFusionTreasure>().add(typeCountSchema).add({ Sockets: Number });
const invasionProgressSchema = new Schema<IInvasionProgressDatabase>(
{
invasionId: Schema.Types.ObjectId,
Delta: Number,
AttackerScore: Number,
DefenderScore: Number
},
{ _id: false }
);
invasionProgressSchema.set("toJSON", {
transform(_doc, obj) {
const db = obj as IInvasionProgressDatabase;
const client = obj as IInvasionProgressClient;
client._id = toOid(db.invasionId);
delete obj.invasionId;
delete obj.__v;
}
});
const spectreLoadoutsSchema = new Schema<ISpectreLoadout>(
{
ItemType: String,
@ -612,6 +727,7 @@ const spectreLoadoutsSchema = new Schema<ISpectreLoadout>(
const weaponSkinsSchema = new Schema<IWeaponSkinDatabase>(
{
ItemType: String,
Favorite: Boolean,
IsNew: Boolean
},
{ id: false }
@ -665,6 +781,26 @@ const loreFragmentScansSchema = new Schema<ILoreFragmentScan>(
{ _id: false }
);
// const lotusCustomizationSchema = new Schema<ILotusCustomization>().add(ItemConfigSchema).add({
// Persona: String
// });
// Laxer schema for cleanupInventory
const lotusCustomizationSchema = new Schema<ILotusCustomization>(
{
Skins: [String],
pricol: colorSchema,
attcol: Schema.Types.Mixed,
sigcol: Schema.Types.Mixed,
eyecol: Schema.Types.Mixed,
facial: Schema.Types.Mixed,
cloth: Schema.Types.Mixed,
syancol: Schema.Types.Mixed,
Persona: String
},
{ _id: false }
);
const evolutionProgressSchema = new Schema<IEvolutionProgress>(
{
Progress: Number,
@ -682,25 +818,23 @@ const endlessXpProgressSchema = new Schema<IEndlessXpProgress>(
{ _id: false }
);
const crewShipPilotWeaponSchema = new Schema<ICrewShipPilotWeapon>(
const crewShipWeaponEmplacementsSchema = new Schema<ICrewShipWeaponEmplacements>(
{
PRIMARY_A: EquipmentSelectionSchema,
SECONDARY_A: EquipmentSelectionSchema
},
{ _id: false }
);
const crewShipPortGunsSchema = new Schema<ICrewShipPortGuns>(
{
PRIMARY_A: EquipmentSelectionSchema
PRIMARY_B: EquipmentSelectionSchema,
SECONDARY_A: EquipmentSelectionSchema,
SECONDARY_B: EquipmentSelectionSchema
},
{ _id: false }
);
const crewShipWeaponSchema = new Schema<ICrewShipWeapon>(
{
PILOT: crewShipPilotWeaponSchema,
PORT_GUNS: crewShipPortGunsSchema
PILOT: crewShipWeaponEmplacementsSchema,
PORT_GUNS: crewShipWeaponEmplacementsSchema,
STARBOARD_GUNS: crewShipWeaponEmplacementsSchema,
ARTILLERY: crewShipWeaponEmplacementsSchema,
SCANNER: crewShipWeaponEmplacementsSchema
},
{ _id: false }
);
@ -724,7 +858,7 @@ const crewShipCustomizationSchema = new Schema<ICrewShipCustomization>(
const crewShipMemberSchema = new Schema<ICrewShipMemberDatabase>(
{
ItemId: { type: Schema.Types.ObjectId, required: false },
NemesisFingerprint: { type: Number, required: false }
NemesisFingerprint: { type: BigInt, required: false }
},
{ _id: false }
);
@ -773,7 +907,7 @@ const dialogueSchema = new Schema<IDialogueDatabase>(
AvailableGiftDate: Date,
RankUpExpiry: Date,
BountyChemExpiry: Date,
//QueuedDialogues: ???
QueuedDialogues: { type: [String], default: [] },
Gifts: { type: [dialogueGiftSchema], default: [] },
Booleans: { type: [String], default: [] },
Completed: { type: [completedDialogueSchema], default: [] },
@ -796,7 +930,8 @@ dialogueSchema.set("toJSON", {
const dialogueHistorySchema = new Schema<IDialogueHistoryDatabase>(
{
YearIteration: { type: Number, required: true },
YearIteration: Number,
Resets: Number,
Dialogues: { type: [dialogueSchema], required: false }
},
{ _id: false }
@ -872,12 +1007,14 @@ const EquipmentSchema = new Schema<IEquipmentDatabase>(
OffensiveUpgrade: String,
DefensiveUpgrade: String,
UpgradesExpiry: Date,
UmbraDate: Date,
ArchonCrystalUpgrades: { type: [ArchonCrystalUpgradeSchema], default: undefined },
Weapon: crewShipWeaponSchema,
Customization: crewShipCustomizationSchema,
RailjackImage: FlavourItemSchema,
CrewMembers: crewShipMembersSchema,
Details: detailsSchema,
Favorite: Boolean,
IsNew: Boolean
},
{ id: false }
@ -902,6 +1039,9 @@ EquipmentSchema.set("toJSON", {
if (db.UpgradesExpiry) {
client.UpgradesExpiry = toMongoDate(db.UpgradesExpiry);
}
if (db.UmbraDate) {
client.UmbraDate = toMongoDate(db.UmbraDate);
}
}
});
@ -915,6 +1055,8 @@ const pendingRecipeSchema = new Schema<IPendingRecipeDatabase>(
{
ItemType: String,
CompletionDate: Date,
TargetItemId: String,
TargetFingerprint: String,
LongGuns: { type: [EquipmentSchema], default: undefined },
Pistols: { type: [EquipmentSchema], default: undefined },
Melee: { type: [EquipmentSchema], default: undefined },
@ -942,6 +1084,13 @@ pendingRecipeSchema.set("toJSON", {
}
});
const accoladesSchema = new Schema<IAccolades>(
{
Heirloom: Boolean
},
{ _id: false }
);
const infestedFoundrySchema = new Schema<IInfestedFoundryDatabase>(
{
Name: String,
@ -999,15 +1148,15 @@ const CustomMarkersSchema = new Schema<ICustomMarkers>(
const calenderProgressSchema = new Schema<ICalendarProgress>(
{
Version: { type: Number, default: 19 },
Iteration: { type: Number, default: 2 },
Iteration: { type: Number, required: true },
YearProgress: {
Upgrades: { type: [] }
Upgrades: { type: [String], default: [] }
},
SeasonProgress: {
SeasonType: String,
LastCompletedDayIdx: { type: Number, default: -1 },
LastCompletedChallengeDayIdx: { type: Number, default: -1 },
ActivatedChallenges: []
SeasonType: { type: String, required: true },
LastCompletedDayIdx: { type: Number, default: 0 },
LastCompletedChallengeDayIdx: { type: Number, default: 0 },
ActivatedChallenges: { type: [String], default: [] }
}
},
{ _id: false }
@ -1129,11 +1278,11 @@ const nemesisSchema = new Schema<INemesisDatabase>(
PrevOwners: Number,
SecondInCommand: Boolean,
Weakened: Boolean,
InfNodes: [infNodeSchema],
InfNodes: { type: [infNodeSchema], default: undefined },
HenchmenKilled: Number,
HintProgress: Number,
Hints: [Number],
GuessHistory: [Number],
Hints: { type: [Number], default: undefined },
GuessHistory: { type: [Number], default: undefined },
MissionCount: Number,
LastEnc: Number
},
@ -1161,6 +1310,36 @@ const alignmentSchema = new Schema<IAlignment>(
{ _id: false }
);
const lastSortieRewardSchema = new Schema<ILastSortieRewardDatabase>(
{
SortieId: Schema.Types.ObjectId,
StoreItem: String,
Manifest: String
},
{ _id: false }
);
lastSortieRewardSchema.set("toJSON", {
virtuals: true,
transform(_doc, obj) {
const db = obj as ILastSortieRewardDatabase;
const client = obj as ILastSortieRewardClient;
client.SortieId = toOid(db.SortieId);
delete obj._id;
delete obj.__v;
}
});
const sortieRewardAttenutationSchema = new Schema<ISortieRewardAttenuation>(
{
Tag: String,
Atten: Number
},
{ _id: false }
);
const lockedWeaponGroupSchema = new Schema<ILockedWeaponGroupDatabase>(
{
s: Schema.Types.ObjectId,
@ -1172,12 +1351,21 @@ const lockedWeaponGroupSchema = new Schema<ILockedWeaponGroupDatabase>(
{ _id: false }
);
const hubNpcCustomizationSchema = new Schema<IHubNpcCustomization>(
{
Colors: colorSchema,
Pattern: String,
Tag: String
},
{ _id: false }
);
const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
{
accountOwnerId: Schema.Types.ObjectId,
SubscribedToEmails: { type: Number, default: 0 },
SubscribedToEmailsPersonalized: { type: Number, default: 0 },
RewardSeed: Number,
RewardSeed: BigInt,
//Credit
RegularCredits: { type: Number, default: 0 },
@ -1211,7 +1399,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
//How many Gift do you have left*(gift spends the trade)
GiftsRemaining: { type: Number, default: 8 },
//Curent trade info Giving or Getting items
PendingTrades: [Schema.Types.Mixed],
//PendingTrades: [Schema.Types.Mixed],
//Syndicate currently being pledged to.
SupportedSyndicate: String,
@ -1261,7 +1449,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
KubrowPetEggs: [kubrowPetEggSchema],
//Prints Cat(3 Prints)\Kubrow(2 Prints) Pets
KubrowPetPrints: [Schema.Types.Mixed],
//KubrowPetPrints: [Schema.Types.Mixed],
//Item for EquippedGear example:Scaner,LoadoutTechSummon etc
Consumables: [typeCountSchema],
@ -1298,7 +1486,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
CrewShipSalvagedWeaponSkins: [upgradeSchema],
//RailJack Crew
CrewMembers: [Schema.Types.Mixed],
CrewMembers: [crewMemberSchema],
//Complete Mission\Quests
Missions: [missionSchema],
@ -1307,7 +1495,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
//item like DojoKey or Boss missions key
LevelKeys: [typeCountSchema],
//Active quests
Quests: [Schema.Types.Mixed],
//Quests: [Schema.Types.Mixed],
//Cosmetics like profile glyphs\Kavasa Prime Kubrow Collar\Game Theme etc
FlavourItems: [FlavourItemSchema],
@ -1319,6 +1507,16 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
//Mastery Rank next availability
TrainingDate: { type: Date, default: new Date(0) },
//Accolades
Staff: Boolean,
Founder: Number,
Guide: Number,
Moderator: Boolean,
Partner: Boolean,
Accolades: accoladesSchema,
//Not an accolade but unlocks an extra chat
Counselor: Boolean,
//you saw last played Region when you opened the star map
LastRegionPlayed: String,
@ -1336,7 +1534,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
TauntHistory: { type: [tauntSchema], default: undefined },
//noShow2FA,VisitPrimeVault etc
WebFlags: Schema.Types.Mixed,
//WebFlags: Schema.Types.Mixed,
//Id CompletedAlerts
CompletedAlerts: [String],
@ -1356,9 +1554,9 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
//the color your clan requests like Items/Research/DojoColors/DojoColorPlainsB
ActiveDojoColorResearch: String,
SentientSpawnChanceBoosters: Schema.Types.Mixed,
//SentientSpawnChanceBoosters: Schema.Types.Mixed,
QualifyingInvasions: [Schema.Types.Mixed],
QualifyingInvasions: [invasionProgressSchema],
FactionScores: [Number],
// https://warframe.fandom.com/wiki/Specter_(Tenno)
@ -1378,7 +1576,9 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
//https://warframe.fandom.com/wiki/Sortie
CompletedSorties: [String],
LastSortieReward: [Schema.Types.Mixed],
LastSortieReward: { type: [lastSortieRewardSchema], default: undefined },
LastLiteSortieReward: { type: [lastSortieRewardSchema], default: undefined },
SortieRewardAttenuation: { type: [sortieRewardAttenutationSchema], default: undefined },
// Resource Extractor Drones
Drones: [droneSchema],
@ -1389,10 +1589,10 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
// open location store like EidolonPlainsDiscoverable or OrbVallisCaveDiscoverable
DiscoveredMarkers: [discoveredMarkerSchema],
//Open location mission like "JobId" + "StageCompletions"
CompletedJobs: [Schema.Types.Mixed],
//CompletedJobs: [Schema.Types.Mixed],
//Game mission\ivent score example "Tag": "WaterFight", "Best": 170, "Count": 1258,
PersonalGoalProgress: [Schema.Types.Mixed],
//PersonalGoalProgress: [Schema.Types.Mixed],
//Setting interface Style
ThemeStyle: String,
@ -1410,7 +1610,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
//https://warframe.fandom.com/wiki/Heist
//ProfitTaker(1-4) Example:"LocationTag": "EudicoHeists", "Jobs":Mission name
CompletedJobChains: [completedJobChainsSchema],
CompletedJobChains: { type: [completedJobChainsSchema], default: undefined },
//Night Wave Challenge
SeasonChallengeHistory: [seasonChallengeHistorySchema],
@ -1422,27 +1622,27 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
LibraryActiveDailyTaskInfo: libraryDailyTaskInfoSchema,
//https://warframe.fandom.com/wiki/Invasion
InvasionChainProgress: [Schema.Types.Mixed],
//InvasionChainProgress: [Schema.Types.Mixed],
//CorpusLich or GrineerLich
NemesisAbandonedRewards: { type: [String], default: [] },
Nemesis: nemesisSchema,
NemesisHistory: [Schema.Types.Mixed],
LastNemesisAllySpawnTime: Schema.Types.Mixed,
NemesisHistory: { type: [nemesisSchema], default: undefined },
//LastNemesisAllySpawnTime: Schema.Types.Mixed,
//TradingRulesConfirmed,ShowFriendInvNotifications(Option->Social)
Settings: settingsSchema,
//Railjack craft
//https://warframe.fandom.com/wiki/Rising_Tide
PersonalTechProjects: [Schema.Types.Mixed],
PersonalTechProjects: { type: [personalTechProjectSchema], default: [] },
//Modulars lvl and exp(Railjack|Duviri)
//https://warframe.fandom.com/wiki/Intrinsics
PlayerSkills: { type: playerSkillsSchema, default: {} },
//TradeBannedUntil data
TradeBannedUntil: Schema.Types.Mixed,
//TradeBannedUntil: Schema.Types.Mixed,
//https://warframe.fandom.com/wiki/Helminth
InfestedFoundry: infestedFoundrySchema,
@ -1451,7 +1651,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
//Purchase this new permanent skin from the Lotus customization options in Personal Quarters located in your Orbiter.
//https://warframe.fandom.com/wiki/Lotus#The_New_War
LotusCustomization: Schema.Types.Mixed,
LotusCustomization: { type: lotusCustomizationSchema, default: undefined },
//Progress+Rank+ItemType(ZarimanPumpShotgun)
//https://warframe.fandom.com/wiki/Incarnon
@ -1462,23 +1662,24 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
//Unknown and system
DuviriInfo: DuviriInfoSchema,
LastInventorySync: Schema.Types.ObjectId,
Mailbox: MailboxSchema,
HandlerPoints: Number,
ChallengesFixVersion: Number,
ChallengesFixVersion: { type: Number, default: 6 },
PlayedParkourTutorial: Boolean,
ActiveLandscapeTraps: [Schema.Types.Mixed],
RepVotes: [Schema.Types.Mixed],
LeagueTickets: [Schema.Types.Mixed],
//ActiveLandscapeTraps: [Schema.Types.Mixed],
//RepVotes: [Schema.Types.Mixed],
//LeagueTickets: [Schema.Types.Mixed],
HasContributedToDojo: Boolean,
HWIDProtectEnabled: Boolean,
LoadOutPresets: { type: Schema.Types.ObjectId, ref: "Loadout" },
CurrentLoadOutIds: [Schema.Types.Mixed],
CurrentLoadOutIds: [oidSchema],
RandomUpgradesIdentified: Number,
BountyScore: Number,
ChallengeInstanceStates: [Schema.Types.Mixed],
//ChallengeInstanceStates: [Schema.Types.Mixed],
RecentVendorPurchases: { type: [recentVendorPurchaseSchema], default: undefined },
Robotics: [Schema.Types.Mixed],
UsedDailyDeals: [Schema.Types.Mixed],
//Robotics: [Schema.Types.Mixed],
//UsedDailyDeals: [Schema.Types.Mixed],
CollectibleSeries: { type: [collectibleEntrySchema], default: undefined },
HasResetAccount: { type: Boolean, default: false },
@ -1513,7 +1714,9 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
// G3 + Zanuka
BrandedSuits: { type: [Schema.Types.ObjectId], default: undefined },
LockedWeaponGroup: { type: lockedWeaponGroupSchema, default: undefined }
LockedWeaponGroup: { type: lockedWeaponGroupSchema, default: undefined },
HubNpcCustomizations: { type: [hubNpcCustomizationSchema], default: undefined }
},
{ timestamps: { createdAt: "Created", updatedAt: false } }
);
@ -1557,6 +1760,9 @@ inventorySchema.set("toJSON", {
sn: inventoryDatabase.LockedWeaponGroup.sn ? toOid(inventoryDatabase.LockedWeaponGroup.sn) : undefined
};
}
if (inventoryDatabase.LastInventorySync) {
inventoryResponse.LastInventorySync = toOid(inventoryDatabase.LastInventorySync);
}
}
});
@ -1577,7 +1783,9 @@ export type InventoryDocumentProps = {
QuestKeys: Types.DocumentArray<IQuestKeyDatabase>;
Drones: Types.DocumentArray<IDroneDatabase>;
CrewShipWeaponSkins: Types.DocumentArray<IUpgradeDatabase>;
CrewShipSalvagedWeaponsSkins: Types.DocumentArray<IUpgradeDatabase>;
CrewShipSalvagedWeaponSkins: Types.DocumentArray<IUpgradeDatabase>;
PersonalTechProjects: Types.DocumentArray<IPersonalTechProjectDatabase>;
CrewMembers: Types.DocumentArray<ICrewMemberDatabase>;
} & { [K in TEquipmentKey]: Types.DocumentArray<IEquipmentDatabase> };
// eslint-disable-next-line @typescript-eslint/no-empty-object-type

View File

@ -3,7 +3,7 @@ import { IEquipmentSelection } from "@/src/types/inventoryTypes/commonInventoryT
import { ILoadoutConfigDatabase, ILoadoutDatabase } from "@/src/types/saveLoadoutTypes";
import { Document, Model, Schema, Types, model } from "mongoose";
const oidSchema = new Schema<IOid>(
export const oidSchema = new Schema<IOid>(
{
$oid: String
},

View File

@ -1,4 +1,4 @@
import { IDatabaseAccountJson } from "@/src/types/loginTypes";
import { IDatabaseAccountJson, IIgnore } from "@/src/types/loginTypes";
import { model, Schema, SchemaOptions } from "mongoose";
const opts = {
@ -37,3 +37,13 @@ databaseAccountSchema.set("toJSON", {
});
export const Account = model<IDatabaseAccountJson>("Account", databaseAccountSchema);
const ignoreSchema = new Schema<IIgnore>({
ignorer: Schema.Types.ObjectId,
ignoree: Schema.Types.ObjectId
});
ignoreSchema.index({ ignorer: 1 });
ignoreSchema.index({ ignorer: 1, ignoree: 1 }, { unique: true });
export const Ignore = model<IIgnore>("Ignore", ignoreSchema);

View File

@ -1,14 +1,17 @@
import { toOid } from "@/src/helpers/inventoryHelpers";
import { toMongoDate, toOid } from "@/src/helpers/inventoryHelpers";
import { colorSchema } from "@/src/models/inventoryModels/inventoryModel";
import { IOrbiter, IPersonalRoomsDatabase, PersonalRoomsModelType } from "@/src/types/personalRoomsTypes";
import {
IApartment,
IFavouriteLoadoutDatabase,
IGardening,
IGardeningDatabase,
IPlacedDecosDatabase,
IPictureFrameInfo,
IRoom,
ITailorShopDatabase
ITailorShopDatabase,
IApartmentDatabase,
IPlanterDatabase,
IPlantDatabase,
IPlantClient
} from "@/src/types/shipTypes";
import { Schema, model } from "mongoose";
@ -62,19 +65,64 @@ const roomSchema = new Schema<IRoom>(
{ _id: false }
);
const gardeningSchema = new Schema<IGardening>({
Planters: [Schema.Types.Mixed] //TODO: add when implementing gardening
});
const apartmentSchema = new Schema<IApartment>(
const favouriteLoadoutSchema = new Schema<IFavouriteLoadoutDatabase>(
{
Rooms: [roomSchema],
FavouriteLoadouts: [Schema.Types.Mixed],
Gardening: gardeningSchema // TODO: ensure this is correct
Tag: String,
LoadoutId: Schema.Types.ObjectId
},
{ _id: false }
);
const apartmentDefault: IApartment = {
favouriteLoadoutSchema.set("toJSON", {
virtuals: true,
transform(_document, returnedObject) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
returnedObject.LoadoutId = toOid(returnedObject.LoadoutId);
}
});
const plantSchema = new Schema<IPlantDatabase>(
{
PlantType: String,
EndTime: Date,
PlotIndex: Number
},
{ _id: false }
);
plantSchema.set("toJSON", {
virtuals: true,
transform(_doc, obj) {
const client = obj as IPlantClient;
const db = obj as IPlantDatabase;
client.EndTime = toMongoDate(db.EndTime);
}
});
const planterSchema = new Schema<IPlanterDatabase>(
{
Name: { type: String, required: true },
Plants: { type: [plantSchema], default: [] }
},
{ _id: false }
);
const gardeningSchema = new Schema<IGardeningDatabase>(
{
Planters: { type: [planterSchema], default: [] }
},
{ _id: false }
);
const apartmentSchema = new Schema<IApartmentDatabase>(
{
Rooms: [roomSchema],
FavouriteLoadouts: [favouriteLoadoutSchema],
Gardening: gardeningSchema
},
{ _id: false }
);
const apartmentDefault: IApartmentDatabase = {
Rooms: [
{ Name: "ElevatorLanding", MaxCapacity: 1600 },
{ Name: "ApartmentRoomA", MaxCapacity: 1000 },
@ -83,13 +131,19 @@ const apartmentDefault: IApartment = {
{ Name: "DuviriHallway", MaxCapacity: 1600 }
],
FavouriteLoadouts: [],
Gardening: {}
Gardening: {
Planters: []
}
};
const orbiterSchema = new Schema<IOrbiter>(
{
Features: [String],
Rooms: [roomSchema],
VignetteFish: { type: [String], default: undefined },
FavouriteLoadoutId: Schema.Types.ObjectId,
Wallpaper: String,
Vignette: String,
ContentUrlSignature: { type: String, required: false },
BootLocation: String
},
@ -107,21 +161,6 @@ const orbiterDefault: IOrbiter = {
]
};
const favouriteLoadoutSchema = new Schema<IFavouriteLoadoutDatabase>(
{
Tag: String,
LoadoutId: Schema.Types.ObjectId
},
{ _id: false }
);
favouriteLoadoutSchema.set("toJSON", {
virtuals: true,
transform(_document, returnedObject) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
returnedObject.LoadoutId = toOid(returnedObject.LoadoutId);
}
});
const tailorShopSchema = new Schema<ITailorShopDatabase>(
{
FavouriteLoadouts: [favouriteLoadoutSchema],

View File

@ -4,6 +4,7 @@ import { abortDojoComponentController } from "@/src/controllers/api/abortDojoCom
import { abortDojoComponentDestructionController } from "@/src/controllers/api/abortDojoComponentDestructionController";
import { activateRandomModController } from "@/src/controllers/api/activateRandomModController";
import { addFriendImageController } from "@/src/controllers/api/addFriendImageController";
import { addIgnoredUserController } from "@/src/controllers/api/addIgnoredUserController";
import { addToAllianceController } from "@/src/controllers/api/addToAllianceController";
import { addToGuildController } from "@/src/controllers/api/addToGuildController";
import { arcaneCommonController } from "@/src/controllers/api/arcaneCommonController";
@ -18,6 +19,7 @@ import { claimCompletedRecipeController } from "@/src/controllers/api/claimCompl
import { claimLibraryDailyTaskRewardController } from "@/src/controllers/api/claimLibraryDailyTaskRewardController";
import { clearDialogueHistoryController } from "@/src/controllers/api/clearDialogueHistoryController";
import { clearNewEpisodeRewardController } from "@/src/controllers/api/clearNewEpisodeRewardController";
import { completeCalendarEventController } from "@/src/controllers/api/completeCalendarEventController";
import { completeRandomModChallengeController } from "@/src/controllers/api/completeRandomModChallengeController";
import { confirmAllianceInvitationController } from "@/src/controllers/api/confirmAllianceInvitationController";
import { confirmGuildInvitationGetController, confirmGuildInvitationPostController } from "@/src/controllers/api/confirmGuildInvitationController";
@ -27,6 +29,8 @@ import { contributeToVaultController } from "@/src/controllers/api/contributeToV
import { createAllianceController } from "@/src/controllers/api/createAllianceController";
import { createGuildController } from "@/src/controllers/api/createGuildController";
import { creditsController } from "@/src/controllers/api/creditsController";
import { crewMembersController } from "@/src/controllers/api/crewMembersController";
import { crewShipIdentifySalvageController } from "@/src/controllers/api/crewShipIdentifySalvageController";
import { customizeGuildRanksController } from "@/src/controllers/api/customizeGuildRanksController";
import { customObstacleCourseLeaderboardController } from "@/src/controllers/api/customObstacleCourseLeaderboardController";
import { declineAllianceInviteController } from "@/src/controllers/api/declineAllianceInviteController";
@ -35,7 +39,7 @@ import { deleteSessionController } from "@/src/controllers/api/deleteSessionCont
import { destroyDojoDecoController } from "@/src/controllers/api/destroyDojoDecoController";
import { divvyAllianceVaultController } from "@/src/controllers/api/divvyAllianceVaultController";
import { dojoComponentRushController } from "@/src/controllers/api/dojoComponentRushController";
import { dojoController } from "@/src/controllers/api/dojoController";
import { dojoController, setDojoURLController } from "@/src/controllers/api/dojoController";
import { dronesController } from "@/src/controllers/api/dronesController";
import { endlessXpController } from "@/src/controllers/api/endlessXpController";
import { entratiLabConquestModeController } from "@/src/controllers/api/entratiLabConquestModeController";
@ -44,6 +48,7 @@ import { findSessionsController } from "@/src/controllers/api/findSessionsContro
import { fishmongerController } from "@/src/controllers/api/fishmongerController";
import { focusController } from "@/src/controllers/api/focusController";
import { fusionTreasuresController } from "@/src/controllers/api/fusionTreasuresController";
import { gardeningController } from "@/src/controllers/api/gardeningController";
import { genericUpdateController } from "@/src/controllers/api/genericUpdateController";
import { getAllianceController } from "@/src/controllers/api/getAllianceController";
import { getDailyDealStockLevelsController } from "@/src/controllers/api/getDailyDealStockLevelsController";
@ -61,7 +66,8 @@ import { giftingController } from "@/src/controllers/api/giftingController";
import { gildWeaponController } from "@/src/controllers/api/gildWeaponController";
import { giveKeyChainTriggeredItemsController } from "@/src/controllers/api/giveKeyChainTriggeredItemsController";
import { giveKeyChainTriggeredMessageController } from "@/src/controllers/api/giveKeyChainTriggeredMessageController";
import { giveQuestKeyRewardController } from "@/src/controllers/api/giveQuestKey";
import { giveQuestKeyRewardController } from "@/src/controllers/api/giveQuestKeyRewardController";
import { giveShipDecoAndLoreFragmentController } from "@/src/controllers/api/giveShipDecoAndLoreFragmentController";
import { giveStartingGearController } from "@/src/controllers/api/giveStartingGearController";
import { guildTechController } from "@/src/controllers/api/guildTechController";
import { hostSessionController } from "@/src/controllers/api/hostSessionController";
@ -85,6 +91,7 @@ import { modularWeaponSaleController } from "@/src/controllers/api/modularWeapon
import { nameWeaponController } from "@/src/controllers/api/nameWeaponController";
import { nemesisController } from "@/src/controllers/api/nemesisController";
import { placeDecoInComponentController } from "@/src/controllers/api/placeDecoInComponentController";
import { playedParkourTutorialController } from "@/src/controllers/api/playedParkourTutorialController";
import { playerSkillsController } from "@/src/controllers/api/playerSkillsController";
import { postGuildAdvertisementController } from "@/src/controllers/api/postGuildAdvertisementController";
import { projectionManagerController } from "@/src/controllers/api/projectionManagerController";
@ -94,23 +101,29 @@ import { redeemPromoCodeController } from "@/src/controllers/api/redeemPromoCode
import { releasePetController } from "@/src/controllers/api/releasePetController";
import { removeFromAllianceController } from "@/src/controllers/api/removeFromAllianceController";
import { removeFromGuildController } from "@/src/controllers/api/removeFromGuildController";
import { removeIgnoredUserController } from "@/src/controllers/api/removeIgnoredUserController";
import { rerollRandomModController } from "@/src/controllers/api/rerollRandomModController";
import { retrievePetFromStasisController } from "@/src/controllers/api/retrievePetFromStasisController";
import { saveDialogueController } from "@/src/controllers/api/saveDialogueController";
import { saveLoadoutController } from "@/src/controllers/api/saveLoadout";
import { saveLoadoutController } from "@/src/controllers/api/saveLoadoutController";
import { saveSettingsController } from "@/src/controllers/api/saveSettingsController";
import { saveVaultAutoContributeController } from "@/src/controllers/api/saveVaultAutoContributeController";
import { sellController } from "@/src/controllers/api/sellController";
import { sendMsgToInBoxController } from "@/src/controllers/api/sendMsgToInBoxController";
import { setActiveQuestController } from "@/src/controllers/api/setActiveQuestController";
import { setActiveShipController } from "@/src/controllers/api/setActiveShipController";
import { setAllianceGuildPermissionsController } from "@/src/controllers/api/setAllianceGuildPermissionsController";
import { setBootLocationController } from "@/src/controllers/api/setBootLocationController";
import { setDojoComponentColorsController } from "@/src/controllers/api/setDojoComponentColorsController";
import { setDojoComponentMessageController } from "@/src/controllers/api/setDojoComponentMessageController";
import { setDojoComponentSettingsController } from "@/src/controllers/api/setDojoComponentSettingsController";
import { setEquippedInstrumentController } from "@/src/controllers/api/setEquippedInstrumentController";
import { setGuildMotdController } from "@/src/controllers/api/setGuildMotdController";
import { setHubNpcCustomizationsController } from "@/src/controllers/api/setHubNpcCustomizationsController";
import { setPlacedDecoInfoController } from "@/src/controllers/api/setPlacedDecoInfoController";
import { setShipCustomizationsController } from "@/src/controllers/api/setShipCustomizationsController";
import { setShipFavouriteLoadoutController } from "@/src/controllers/api/setShipFavouriteLoadoutController";
import { setShipVignetteController } from "@/src/controllers/api/setShipVignetteController";
import { setSupportedSyndicateController } from "@/src/controllers/api/setSupportedSyndicateController";
import { setWeaponSkillTreeController } from "@/src/controllers/api/setWeaponSkillTreeController";
import { shipDecorationsController } from "@/src/controllers/api/shipDecorationsController";
@ -143,9 +156,11 @@ const apiRouter = express.Router();
apiRouter.get("/abandonLibraryDailyTask.php", abandonLibraryDailyTaskController);
apiRouter.get("/abortDojoComponentDestruction.php", abortDojoComponentDestructionController);
apiRouter.get("/cancelGuildAdvertisement.php", cancelGuildAdvertisementController);
apiRouter.get("/changeDojoRoot.php", changeDojoRootController);
apiRouter.get("/changeGuildRank.php", changeGuildRankController);
apiRouter.get("/checkDailyMissionBonus.php", checkDailyMissionBonusController);
apiRouter.get("/claimLibraryDailyTaskReward.php", claimLibraryDailyTaskRewardController);
apiRouter.get("/completeCalendarEvent.php", completeCalendarEventController);
apiRouter.get("/confirmAllianceInvitation.php", confirmAllianceInvitationController);
apiRouter.get("/confirmGuildInvitation.php", confirmGuildInvitationGetController);
apiRouter.get("/credits.php", creditsController);
@ -174,12 +189,14 @@ apiRouter.get("/logout.php", logoutController);
apiRouter.get("/marketRecommendations.php", marketRecommendationsController);
apiRouter.get("/marketSearchRecommendations.php", marketRecommendationsController);
apiRouter.get("/modularWeaponSale.php", modularWeaponSaleController);
apiRouter.get("/playedParkourTutorial.php", playedParkourTutorialController);
apiRouter.get("/queueDojoComponentDestruction.php", queueDojoComponentDestructionController);
apiRouter.get("/removeFromAlliance.php", removeFromAllianceController);
apiRouter.get("/setActiveQuest.php", setActiveQuestController);
apiRouter.get("/setActiveShip.php", setActiveShipController);
apiRouter.get("/setAllianceGuildPermissions.php", setAllianceGuildPermissionsController);
apiRouter.get("/setBootLocation.php", setBootLocationController);
apiRouter.get("/setDojoURL", setDojoURLController);
apiRouter.get("/setGuildMotd.php", setGuildMotdController);
apiRouter.get("/setSupportedSyndicate.php", setSupportedSyndicateController);
apiRouter.get("/startLibraryDailyTask.php", startLibraryDailyTaskController);
@ -192,6 +209,7 @@ apiRouter.get("/updateSession.php", updateSessionGetController);
apiRouter.post("/abortDojoComponent.php", abortDojoComponentController);
apiRouter.post("/activateRandomMod.php", activateRandomModController);
apiRouter.post("/addFriendImage.php", addFriendImageController);
apiRouter.post("/addIgnoredUser.php", addIgnoredUserController);
apiRouter.post("/addToAlliance.php", addToAllianceController);
apiRouter.post("/addToGuild.php", addToGuildController);
apiRouter.post("/arcaneCommon.php", arcaneCommonController);
@ -209,6 +227,8 @@ apiRouter.post("/contributeToDojoComponent.php", contributeToDojoComponentContro
apiRouter.post("/contributeToVault.php", contributeToVaultController);
apiRouter.post("/createAlliance.php", createAllianceController);
apiRouter.post("/createGuild.php", createGuildController);
apiRouter.post("/crewMembers.php", crewMembersController);
apiRouter.post("/crewShipIdentifySalvage.php", crewShipIdentifySalvageController);
apiRouter.post("/customizeGuildRanks.php", customizeGuildRanksController);
apiRouter.post("/customObstacleCourseLeaderboard.php", customObstacleCourseLeaderboardController);
apiRouter.post("/destroyDojoDeco.php", destroyDojoDecoController);
@ -221,6 +241,7 @@ apiRouter.post("/findSessions.php", findSessionsController);
apiRouter.post("/fishmonger.php", fishmongerController);
apiRouter.post("/focus.php", focusController);
apiRouter.post("/fusionTreasures.php", fusionTreasuresController);
apiRouter.post("/gardening.php", gardeningController);
apiRouter.post("/genericUpdate.php", genericUpdateController);
apiRouter.post("/getAlliance.php", getAllianceController);
apiRouter.post("/getFriends.php", getFriendsController);
@ -231,6 +252,7 @@ apiRouter.post("/gildWeapon.php", gildWeaponController);
apiRouter.post("/giveKeyChainTriggeredItems.php", giveKeyChainTriggeredItemsController);
apiRouter.post("/giveKeyChainTriggeredMessage.php", giveKeyChainTriggeredMessageController);
apiRouter.post("/giveQuestKeyReward.php", giveQuestKeyRewardController);
apiRouter.post("/giveShipDecoAndLoreFragment.php", giveShipDecoAndLoreFragmentController);
apiRouter.post("/giveStartingGear.php", giveStartingGearController);
apiRouter.post("/guildTech.php", guildTechController);
apiRouter.post("/hostSession.php", hostSessionController);
@ -254,6 +276,7 @@ apiRouter.post("/purchase.php", purchaseController);
apiRouter.post("/redeemPromoCode.php", redeemPromoCodeController);
apiRouter.post("/releasePet.php", releasePetController);
apiRouter.post("/removeFromGuild.php", removeFromGuildController);
apiRouter.post("/removeIgnoredUser.php", removeIgnoredUserController);
apiRouter.post("/rerollRandomMod.php", rerollRandomModController);
apiRouter.post("/retrievePetFromStasis.php", retrievePetFromStasisController);
apiRouter.post("/saveDialogue.php", saveDialogueController);
@ -261,12 +284,17 @@ apiRouter.post("/saveLoadout.php", saveLoadoutController);
apiRouter.post("/saveSettings.php", saveSettingsController);
apiRouter.post("/saveVaultAutoContribute.php", saveVaultAutoContributeController);
apiRouter.post("/sell.php", sellController);
apiRouter.post("/sendMsgToInBox.php", sendMsgToInBoxController);
apiRouter.post("/setDojoComponentColors.php", setDojoComponentColorsController);
apiRouter.post("/setDojoComponentMessage.php", setDojoComponentMessageController);
apiRouter.post("/setDojoComponentSettings.php", setDojoComponentSettingsController);
apiRouter.post("/setEquippedInstrument.php", setEquippedInstrumentController);
apiRouter.post("/setGuildMotd.php", setGuildMotdController);
apiRouter.post("/setHubNpcCustomizations.php", setHubNpcCustomizationsController);
apiRouter.post("/setPlacedDecoInfo.php", setPlacedDecoInfoController);
apiRouter.post("/setShipCustomizations.php", setShipCustomizationsController);
apiRouter.post("/setShipFavouriteLoadout.php", setShipFavouriteLoadoutController);
apiRouter.post("/setShipVignette.php", setShipVignetteController);
apiRouter.post("/setWeaponSkillTree.php", setWeaponSkillTreeController);
apiRouter.post("/shipDecorations.php", shipDecorationsController);
apiRouter.post("/startCollectibleEntry.php", startCollectibleEntryController);

View File

@ -10,19 +10,19 @@ import { getAccountInfoController } from "@/src/controllers/custom/getAccountInf
import { renameAccountController } from "@/src/controllers/custom/renameAccountController";
import { ircDroppedController } from "@/src/controllers/custom/ircDroppedController";
import { unlockAllIntrinsicsController } from "@/src/controllers/custom/unlockAllIntrinsicsController";
import { addMissingMaxRankModsController } from "@/src/controllers/custom/addMissingMaxRankModsController";
import { createAccountController } from "@/src/controllers/custom/createAccountController";
import { createMessageController } from "@/src/controllers/custom/createMessageController";
import { addCurrencyController } from "@/src/controllers/custom/addCurrencyController";
import { addItemsController } from "@/src/controllers/custom/addItemsController";
import { addModularEquipmentController } from "@/src/controllers/custom/addModularEquipmentController";
import { addXpController } from "@/src/controllers/custom/addXpController";
import { gildEquipmentController } from "@/src/controllers/custom/gildEquipmentController";
import { importController } from "@/src/controllers/custom/importController";
import { manageQuestsController } from "@/src/controllers/custom/manageQuestsController";
import { setEvolutionProgressController } from "@/src/controllers/custom/setEvolutionProgressController";
import { getConfigDataController } from "@/src/controllers/custom/getConfigDataController";
import { updateConfigDataController } from "@/src/controllers/custom/updateConfigDataController";
import { manageQuestsController } from "@/src/controllers/custom/manageQuestsController";
const customRouter = express.Router();
@ -36,16 +36,16 @@ customRouter.get("/getAccountInfo", getAccountInfoController);
customRouter.get("/renameAccount", renameAccountController);
customRouter.get("/ircDropped", ircDroppedController);
customRouter.get("/unlockAllIntrinsics", unlockAllIntrinsicsController);
customRouter.get("/addMissingMaxRankMods", addMissingMaxRankModsController);
customRouter.post("/createAccount", createAccountController);
customRouter.post("/createMessage", createMessageController);
customRouter.post("/addCurrency", addCurrencyController);
customRouter.post("/addItems", addItemsController);
customRouter.post("/addModularEquipment", addModularEquipmentController);
customRouter.post("/addXp", addXpController);
customRouter.post("/gildEquipment", gildEquipmentController);
customRouter.post("/import", importController);
customRouter.post("/manageQuests", manageQuestsController);
customRouter.post("/setEvolutionProgress", setEvolutionProgressController);
customRouter.get("/config", getConfigDataController);
customRouter.post("/config", updateConfigDataController);

View File

@ -24,21 +24,29 @@ interface IConfig {
infiniteEndo?: boolean;
infiniteRegalAya?: boolean;
infiniteHelminthMaterials?: boolean;
dontSubtractConsumables?: boolean;
unlockAllShipFeatures?: boolean;
unlockAllShipDecorations?: boolean;
unlockAllFlavourItems?: boolean;
unlockAllSkins?: boolean;
unlockAllCapturaScenes?: boolean;
unlockAllDecoRecipes?: boolean;
universalPolarityEverywhere?: boolean;
unlockDoubleCapacityPotatoesEverywhere?: boolean;
unlockExilusEverywhere?: boolean;
unlockArcanesEverywhere?: boolean;
noDailyStandingLimits?: boolean;
noDailyFocusLimit?: boolean;
noArgonCrystalDecay?: boolean;
noMasteryRankUpCooldown?: boolean;
noVendorPurchaseLimits?: boolean;
noDeathMarks?: boolean;
noKimCooldowns?: boolean;
instantResourceExtractorDrones?: boolean;
noResourceExtractorDronesDamage?: boolean;
skipClanKeyCrafting?: boolean;
noDojoRoomBuildStage?: boolean;
noDojoDecoBuildStage?: boolean;
fastDojoRoomDestruction?: boolean;
noDojoResearchCosts?: boolean;
noDojoResearchTime?: boolean;

View File

@ -1,6 +1,6 @@
import { Request } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getInventory } from "@/src/services/inventoryService";
import { addLevelKeys, addRecipes, combineInventoryChanges, getInventory } from "@/src/services/inventoryService";
import { Alliance, AllianceMember, Guild, GuildAd, GuildMember, TGuildDatabaseDocument } from "@/src/models/guildModel";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import {
@ -30,6 +30,8 @@ import { Inbox } from "../models/inboxModel";
import { IFusionTreasure, ITypeCount } from "../types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "../types/purchaseTypes";
import { parallelForeach } from "../utils/async-utils";
import allDecoRecipes from "@/static/fixed_responses/allDecoRecipes.json";
import { createMessage } from "./inboxService";
export const getGuildForRequest = async (req: Request): Promise<TGuildDatabaseDocument> => {
const accountId = await getAccountIdForRequest(req);
@ -57,6 +59,7 @@ export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: s
const members: IGuildMemberClient[] = [];
let missingEntry = true;
const dataFillInPromises: Promise<void>[] = [];
for (const guildMember of guildMembers) {
const member: IGuildMemberClient = {
_id: toOid(guildMember.accountId),
@ -68,8 +71,12 @@ export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: s
if (guildMember.accountId.equals(accountId)) {
missingEntry = false;
} else {
member.DisplayName = (await Account.findById(guildMember.accountId, "DisplayName"))!.DisplayName;
await fillInInventoryDataForGuildMember(member);
dataFillInPromises.push(
(async (): Promise<void> => {
member.DisplayName = (await Account.findById(guildMember.accountId, "DisplayName"))!.DisplayName;
})()
);
dataFillInPromises.push(fillInInventoryDataForGuildMember(member));
}
members.push(member);
}
@ -88,6 +95,8 @@ export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: s
});
}
await Promise.all(dataFillInPromises);
return {
_id: toOid(guild._id),
Name: guild.Name,
@ -95,8 +104,8 @@ export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: s
LongMOTD: guild.LongMOTD,
Members: members,
Ranks: guild.Ranks,
TradeTax: guild.TradeTax,
Tier: guild.Tier,
Emblem: guild.Emblem,
Vault: getGuildVault(guild),
ActiveDojoColorResearch: guild.ActiveDojoColorResearch,
Class: guild.Class,
@ -104,6 +113,7 @@ export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: s
IsContributor: !!guild.CeremonyContributors?.find(x => x.equals(accountId)),
NumContributors: guild.CeremonyContributors?.length ?? 0,
CeremonyResetDate: guild.CeremonyResetDate ? toMongoDate(guild.CeremonyResetDate) : undefined,
AutoContributeFromVault: guild.AutoContributeFromVault,
AllianceId: guild.AllianceId ? toOid(guild.AllianceId) : undefined
};
};
@ -114,19 +124,26 @@ export const getGuildVault = (guild: TGuildDatabaseDocument): IGuildVault => {
DojoRefundMiscItems: guild.VaultMiscItems,
DojoRefundPremiumCredits: guild.VaultPremiumCredits,
ShipDecorations: guild.VaultShipDecorations,
FusionTreasures: guild.VaultFusionTreasures
FusionTreasures: guild.VaultFusionTreasures,
DecoRecipes: config.unlockAllDecoRecipes
? allDecoRecipes.map(recipe => ({ ItemType: recipe, ItemCount: 1 }))
: guild.VaultDecoRecipes
};
};
export const getDojoClient = async (
guild: TGuildDatabaseDocument,
status: number,
componentId: Types.ObjectId | string | undefined = undefined
componentId?: Types.ObjectId | string
): Promise<IDojoClient> => {
const dojo: IDojoClient = {
_id: { $oid: guild._id.toString() },
Name: guild.Name,
Tier: 1,
Tier: guild.Tier,
GuildEmblem: guild.Emblem,
TradeTax: guild.TradeTax,
NumContributors: guild.CeremonyContributors?.length ?? 0,
CeremonyResetDate: guild.CeremonyResetDate ? toMongoDate(guild.CeremonyResetDate) : undefined,
FixedContributions: true,
DojoRevision: 1,
Vault: getGuildVault(guild),
@ -137,16 +154,19 @@ export const getDojoClient = async (
DojoComponents: []
};
const roomsToRemove: Types.ObjectId[] = [];
const decosToRemoveNoRefund: { componentId: Types.ObjectId; decoId: Types.ObjectId }[] = [];
let needSave = false;
for (const dojoComponent of guild.DojoComponents) {
if (!componentId || dojoComponent._id.equals(componentId)) {
const clientComponent: IDojoComponentClient = {
id: toOid(dojoComponent._id),
SortId: toOid(dojoComponent.SortId ?? dojoComponent._id), // always providing a SortId so decos don't need repositioning to reparent
pf: dojoComponent.pf,
ppf: dojoComponent.ppf,
Name: dojoComponent.Name,
Message: dojoComponent.Message,
DecoCapacity: dojoComponent.DecoCapacity ?? 600
DecoCapacity: dojoComponent.DecoCapacity ?? 600,
Settings: dojoComponent.Settings
};
if (dojoComponent.pi) {
clientComponent.pi = toOid(dojoComponent.pi);
@ -203,11 +223,27 @@ export const getDojoClient = async (
Type: deco.Type,
Pos: deco.Pos,
Rot: deco.Rot,
Scale: deco.Scale,
Name: deco.Name,
Sockets: deco.Sockets,
PictureFrameInfo: deco.PictureFrameInfo
};
if (deco.CompletionTime) {
if (
deco.Type == "/Lotus/Objects/Tenno/Props/TnoPaintBotDojoDeco" &&
Date.now() >= deco.CompletionTime.getTime()
) {
if (dojoComponent.PendingColors) {
dojoComponent.Colors = dojoComponent.PendingColors;
dojoComponent.PendingColors = undefined;
}
if (dojoComponent.PendingLights) {
dojoComponent.Lights = dojoComponent.PendingLights;
dojoComponent.PendingLights = undefined;
}
decosToRemoveNoRefund.push({ componentId: dojoComponent._id, decoId: deco._id });
continue;
}
clientDeco.CompletionTime = toMongoDate(deco.CompletionTime);
} else {
clientDeco.RegularCredits = deco.RegularCredits;
@ -216,6 +252,10 @@ export const getDojoClient = async (
clientComponent.Decos.push(clientDeco);
}
}
clientComponent.PendingColors = dojoComponent.PendingColors;
clientComponent.Colors = dojoComponent.Colors;
clientComponent.PendingLights = dojoComponent.PendingLights;
clientComponent.Lights = dojoComponent.Lights;
dojo.DojoComponents.push(clientComponent);
}
}
@ -226,6 +266,15 @@ export const getDojoClient = async (
}
needSave = true;
}
for (const deco of decosToRemoveNoRefund) {
logger.debug(`removing polychrome`, deco);
const component = guild.DojoComponents.id(deco.componentId)!;
component.Decos!.splice(
component.Decos!.findIndex(x => x._id.equals(deco.decoId)),
1
);
needSave = true;
}
if (needSave) {
await guild.save();
}
@ -456,7 +505,7 @@ export const hasGuildPermissionEx = (
export const removePigmentsFromGuildMembers = async (guildId: string | Types.ObjectId): Promise<void> => {
const members = await GuildMember.find({ guildId, status: 0 }, "accountId");
for (const member of members) {
await parallelForeach(members, async member => {
const inventory = await getInventory(member.accountId.toString(), "MiscItems");
const index = inventory.MiscItems.findIndex(
x => x.ItemType == "/Lotus/Types/Items/Research/DojoColors/GenericDojoColorPigment"
@ -465,7 +514,7 @@ export const removePigmentsFromGuildMembers = async (guildId: string | Types.Obj
inventory.MiscItems.splice(index, 1);
await inventory.save();
}
}
});
};
export const processGuildTechProjectContributionsUpdate = async (
@ -505,7 +554,7 @@ export const setGuildTechLogState = (
guild: TGuildDatabaseDocument,
type: string,
state: number,
dateTime: Date | undefined = undefined
dateTime?: Date
): boolean => {
guild.TechChanges ??= [];
const entry = guild.TechChanges.find(x => x.details == type);
@ -562,6 +611,76 @@ const setGuildTier = async (guild: TGuildDatabaseDocument, newTier: number): Pro
await processGuildTechProjectContributionsUpdate(guild, project);
}
}
if (guild.CeremonyContributors) {
await checkClanAscensionHasRequiredContributors(guild);
}
};
export const checkClanAscensionHasRequiredContributors = async (guild: TGuildDatabaseDocument): Promise<void> => {
const requiredContributors = [1, 5, 15, 30, 50][guild.Tier - 1];
// Once required contributor count is hit, the class is committed and there's 72 hours to claim endo.
if (guild.CeremonyContributors!.length >= 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: guild._id, status: 0 }, "accountId");
await parallelForeach(members, async member => {
// 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, [
{
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
}
]);
});
}
}
};
export const giveClanKey = (inventory: TInventoryDatabaseDocument, inventoryChanges?: IInventoryChanges): void => {
if (config.skipClanKeyCrafting) {
const levelKeyChanges = [
{
ItemType: "/Lotus/Types/Keys/DojoKey",
ItemCount: 1
}
];
addLevelKeys(inventory, levelKeyChanges);
if (inventoryChanges) {
combineInventoryChanges(inventoryChanges, { LevelKeys: levelKeyChanges });
}
} else {
const recipeChanges = [
{
ItemType: "/Lotus/Types/Keys/DojoKeyBlueprint",
ItemCount: 1
}
];
addRecipes(inventory, recipeChanges);
if (inventoryChanges) {
combineInventoryChanges(inventoryChanges, { Recipes: recipeChanges });
}
}
};
export const removeDojoKeyItems = (inventory: TInventoryDatabaseDocument): IInventoryChanges => {

View File

@ -2,6 +2,7 @@ import { Types } from "mongoose";
import {
IEquipmentClient,
IEquipmentDatabase,
IItemConfig,
IOperatorConfigClient,
IOperatorConfigDatabase
} from "../types/inventoryTypes/commonInventoryTypes";
@ -37,6 +38,7 @@ import {
} from "../types/inventoryTypes/inventoryTypes";
import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel";
import { ILoadoutConfigDatabase, ILoadoutDatabase } from "../types/saveLoadoutTypes";
import { slotNames } from "../types/purchaseTypes";
const convertDate = (value: IMongoDate): Date => {
return new Date(parseInt(value.$date.$numberLong));
@ -54,6 +56,7 @@ const convertEquipment = (client: IEquipmentClient): IEquipmentDatabase => {
InfestationDate: convertOptionalDate(client.InfestationDate),
Expiry: convertOptionalDate(client.Expiry),
UpgradesExpiry: convertOptionalDate(client.UpgradesExpiry),
UmbraDate: convertOptionalDate(client.UmbraDate),
CrewMembers: client.CrewMembers ? convertCrewShipMembers(client.CrewMembers) : undefined,
Details: client.Details ? convertKubrowDetails(client.Details) : undefined,
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
@ -103,18 +106,18 @@ const replaceSlots = (db: ISlots, client: ISlots): void => {
db.Slots = client.Slots;
};
const convertCrewShipMember = (client: ICrewShipMemberClient): ICrewShipMemberDatabase => {
return {
...client,
ItemId: client.ItemId ? new Types.ObjectId(client.ItemId.$oid) : undefined
};
export const importCrewMemberId = (crewMemberId: ICrewShipMemberClient): ICrewShipMemberDatabase => {
if (crewMemberId.ItemId) {
return { ItemId: new Types.ObjectId(crewMemberId.ItemId.$oid) };
}
return { NemesisFingerprint: BigInt(crewMemberId.NemesisFingerprint ?? 0) };
};
const convertCrewShipMembers = (client: ICrewShipMembersClient): ICrewShipMembersDatabase => {
return {
SLOT_A: client.SLOT_A ? convertCrewShipMember(client.SLOT_A) : undefined,
SLOT_B: client.SLOT_B ? convertCrewShipMember(client.SLOT_B) : undefined,
SLOT_C: client.SLOT_C ? convertCrewShipMember(client.SLOT_C) : undefined
SLOT_A: client.SLOT_A ? importCrewMemberId(client.SLOT_A) : undefined,
SLOT_B: client.SLOT_B ? importCrewMemberId(client.SLOT_B) : undefined,
SLOT_C: client.SLOT_C ? importCrewMemberId(client.SLOT_C) : undefined
};
};
@ -167,10 +170,25 @@ const convertPendingRecipe = (client: IPendingRecipeClient): IPendingRecipeDatab
const convertNemesis = (client: INemesisClient): INemesisDatabase => {
return {
...client,
fp: BigInt(client.fp),
d: convertDate(client.d)
};
};
// Empty objects from live may have been encoded as empty arrays because of PHP.
const convertItemConfig = <T extends IItemConfig>(client: T): T => {
return {
...client,
pricol: Array.isArray(client.pricol) ? {} : client.pricol,
attcol: Array.isArray(client.attcol) ? {} : client.attcol,
sigcol: Array.isArray(client.sigcol) ? {} : client.sigcol,
eyecol: Array.isArray(client.eyecol) ? {} : client.eyecol,
facial: Array.isArray(client.facial) ? {} : client.facial,
cloth: Array.isArray(client.cloth) ? {} : client.cloth,
syancol: Array.isArray(client.syancol) ? {} : client.syancol
};
};
export const importInventory = (db: TInventoryDatabaseDocument, client: Partial<IInventoryClient>): void => {
for (const key of equipmentKeys) {
if (client[key] !== undefined) {
@ -211,35 +229,28 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial<
replaceArray<IOperatorConfigDatabase>(db[key], client[key].map(convertOperatorConfig));
}
}
for (const key of [
"SuitBin",
"WeaponBin",
"SentinelBin",
"SpaceSuitBin",
"SpaceWeaponBin",
"PvpBonusLoadoutBin",
"PveBonusLoadoutBin",
"RandomModBin",
"MechBin",
"CrewMemberBin",
"OperatorAmpBin",
"CrewShipSalvageBin"
] as const) {
for (const key of slotNames) {
if (client[key] !== undefined) {
replaceSlots(db[key], client[key]);
}
}
// boolean
for (const key of [
"UseAdultOperatorLoadout",
"HasOwnedVoidProjectionsPreviously",
"ReceivedStartingGear",
"ArchwingEnabled",
"PlayedParkourTutorial"
"PlayedParkourTutorial",
"Staff",
"Moderator",
"Partner",
"Counselor"
] as const) {
if (client[key] !== undefined) {
db[key] = client[key];
}
}
// number
for (const key of [
"PlayerLevel",
"RegularCredits",
@ -249,12 +260,15 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial<
"PrimeTokens",
"TradesRemaining",
"GiftsRemaining",
"ChallengesFixVersion"
"ChallengesFixVersion",
"Founder",
"Guide"
] as const) {
if (client[key] !== undefined) {
db[key] = client[key];
}
}
// string
for (const key of [
"ThemeStyle",
"ThemeBackground",
@ -269,6 +283,7 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial<
db[key] = client[key];
}
}
// string[]
for (const key of [
"EquippedGear",
"EquippedEmotes",
@ -352,7 +367,7 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial<
db.PlayerSkills = client.PlayerSkills;
}
if (client.LotusCustomization !== undefined) {
db.LotusCustomization = client.LotusCustomization;
db.LotusCustomization = convertItemConfig(client.LotusCustomization);
}
if (client.CollectibleSeries !== undefined) {
db.CollectibleSeries = client.CollectibleSeries;
@ -379,6 +394,9 @@ export const importInventory = (db: TInventoryDatabaseDocument, client: Partial<
});
});
}
if (client.Accolades !== undefined) {
db.Accolades = client.Accolades;
}
};
const convertLoadOutConfig = (client: ILoadoutConfigClient): ILoadoutConfigDatabase => {

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,5 @@
import { IKeyChainRequest } from "@/src/types/requestTypes";
import { getIndexAfter } from "@/src/helpers/stringHelpers";
import { ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger";
import {
dict_de,
dict_en,
@ -19,6 +17,7 @@ import {
dict_uk,
dict_zh,
ExportArcanes,
ExportBoosters,
ExportCustoms,
ExportDrones,
ExportGear,
@ -34,7 +33,6 @@ import {
IRecipe,
TReward
} from "warframe-public-export-plus";
import questCompletionItems from "@/static/fixed_responses/questCompletionRewards.json";
import { IMessage } from "../models/inboxModel";
export type WeaponTypeInternal =
@ -181,32 +179,6 @@ export const getLevelKeyRewards = (
};
};
export const getQuestCompletionItems = (questKey: string): ITypeCount[] | undefined => {
if (questKey in questCompletionItems) {
return questCompletionItems[questKey as keyof typeof questCompletionItems];
}
logger.warn(`Quest ${questKey} not found in questCompletionItems`);
const items: ITypeCount[] = [];
const meta = ExportKeys[questKey];
if (meta.rewards) {
for (const reward of meta.rewards) {
if (reward.rewardType == "RT_STORE_ITEM") {
items.push({
ItemType: fromStoreItem(reward.itemType),
ItemCount: 1
});
} else if (reward.rewardType == "RT_RESOURCE" || reward.rewardType == "RT_RECIPE") {
items.push({
ItemType: reward.itemType,
ItemCount: reward.amount
});
}
}
}
return items;
};
export const getKeyChainMessage = ({ KeyChain, ChainStage }: IKeyChainRequest): IMessage => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const chainStages = ExportKeys[KeyChain]?.chainStages;
@ -214,14 +186,15 @@ export const getKeyChainMessage = ({ KeyChain, ChainStage }: IKeyChainRequest):
throw new Error(`KeyChain ${KeyChain} does not contain chain stages`);
}
const keyChainStage = chainStages[ChainStage];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!keyChainStage) {
throw new Error(`KeyChainStage ${ChainStage} not found`);
let i = ChainStage;
let chainStageMessage = chainStages[i].messageToSendWhenTriggered;
while (!chainStageMessage) {
if (++i >= chainStages.length) {
break;
}
chainStageMessage = chainStages[i].messageToSendWhenTriggered;
}
const chainStageMessage = keyChainStage.messageToSendWhenTriggered;
if (!chainStageMessage) {
throw new Error(
`client requested key chain message in keychain ${KeyChain} at stage ${ChainStage} but they did not exist`
@ -245,15 +218,30 @@ export const convertInboxMessage = (message: IInboxMessage): IMessage => {
};
export const isStoreItem = (type: string): boolean => {
return type.startsWith("/Lotus/StoreItems/");
return type.startsWith("/Lotus/StoreItems/") || type in ExportBoosters;
};
export const toStoreItem = (type: string): string => {
if (type.startsWith("/Lotus/Types/StoreItems/Boosters/")) {
const boosterEntry = Object.entries(ExportBoosters).find(arr => arr[1].typeName == type);
if (boosterEntry) {
return boosterEntry[0];
}
throw new Error(`could not convert ${type} to a store item`);
}
return "/Lotus/StoreItems/" + type.substring("/Lotus/".length);
};
export const fromStoreItem = (type: string): string => {
return "/Lotus/" + type.substring("/Lotus/StoreItems/".length);
if (type.startsWith("/Lotus/StoreItems/")) {
return "/Lotus/" + type.substring("/Lotus/StoreItems/".length);
}
if (type in ExportBoosters) {
return ExportBoosters[type].typeName;
}
throw new Error(`${type} is not a store item`);
};
export const getDefaultUpgrades = (parts: string[]): IDefaultUpgrade[] | undefined => {

View File

@ -8,7 +8,7 @@ export const submitLeaderboardScore = async (
ownerId: string,
displayName: string,
score: number,
guildId?: string
guildId: string | undefined
): Promise<void> => {
let expiry: Date;
if (schedule == "daily") {
@ -39,9 +39,9 @@ export const getLeaderboard = async (
leaderboard: string,
before: number,
after: number,
pivotId?: string,
guildId?: string,
guildTier?: number
pivotId: string | undefined,
guildId: string | undefined,
guildTier: number | undefined
): Promise<ILeaderboardEntryClient[]> => {
const filter: { leaderboard: string; guildId?: string; guildTier?: number } = { leaderboard };
if (guildId) {

View File

@ -77,7 +77,6 @@ const getRandomLoginReward = (rng: CRng, day: number, inventory: TInventoryDatab
const reward = rng.randomReward(randomRewards)!;
//const reward = randomRewards.find(x => x.RewardType == "RT_BOOSTER")!;
if (reward.RewardType == "RT_RANDOM_RECIPE") {
// Not very faithful implementation but roughly the same idea
const masteredItems = new Set();
for (const entry of inventory.XPInfo) {
masteredItems.add(entry.ItemType);
@ -95,15 +94,15 @@ const getRandomLoginReward = (rng: CRng, day: number, inventory: TInventoryDatab
}
const eligibleRecipes: string[] = [];
for (const [uniqueName, recipe] of Object.entries(ExportRecipes)) {
if (unmasteredItems.has(recipe.resultType)) {
if (!recipe.excludeFromMarket && unmasteredItems.has(recipe.resultType)) {
eligibleRecipes.push(uniqueName);
}
}
if (eligibleRecipes.length == 0) {
// This account has all warframes and weapons already mastered (filthy cheater), need a different reward.
// This account has all applicable warframes and weapons already mastered (filthy cheater), need a different reward.
return getRandomLoginReward(rng, day, inventory);
}
reward.StoreItemType = toStoreItem(rng.randomElement(eligibleRecipes));
reward.StoreItemType = toStoreItem(rng.randomElement(eligibleRecipes)!);
}
return {
//_id: toOid(new Types.ObjectId()),

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,14 @@
import { PersonalRooms } from "@/src/models/personalRoomsModel";
import { addItem, getInventory } from "@/src/services/inventoryService";
import { TPersonalRoomsDatabaseDocument } from "../types/personalRoomsTypes";
import { IGardeningDatabase } from "../types/shipTypes";
import { getRandomElement } from "./rngService";
export const getPersonalRooms = async (accountId: string): Promise<TPersonalRoomsDatabaseDocument> => {
const personalRooms = await PersonalRooms.findOne({ personalRoomsOwnerId: accountId });
export const getPersonalRooms = async (
accountId: string,
projection?: string
): Promise<TPersonalRoomsDatabaseDocument> => {
const personalRooms = await PersonalRooms.findOne({ personalRoomsOwnerId: accountId }, projection);
if (!personalRooms) {
throw new Error(`personal rooms not found for account ${accountId}`);
@ -25,3 +30,64 @@ export const updateShipFeature = async (accountId: string, shipFeature: string):
await addItem(inventory, shipFeature, -1);
await inventory.save();
};
export const createGarden = (): IGardeningDatabase => {
const plantTypes = [
"/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantA",
"/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantB",
"/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantC",
"/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantD",
"/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantE",
"/Lotus/Types/Items/Plants/MiscItems/DuvxDuviriGrowingPlantF"
];
const endTime = new Date((Math.trunc(Date.now() / 1000) + 79200) * 1000); // Plants will take 22 hours to grow
return {
Planters: [
{
Name: "Garden0",
Plants: [
{
PlantType: getRandomElement(plantTypes)!,
EndTime: endTime,
PlotIndex: 0
},
{
PlantType: getRandomElement(plantTypes)!,
EndTime: endTime,
PlotIndex: 1
}
]
},
{
Name: "Garden1",
Plants: [
{
PlantType: getRandomElement(plantTypes)!,
EndTime: endTime,
PlotIndex: 0
},
{
PlantType: getRandomElement(plantTypes)!,
EndTime: endTime,
PlotIndex: 1
}
]
},
{
Name: "Garden2",
Plants: [
{
PlantType: getRandomElement(plantTypes)!,
EndTime: endTime,
PlotIndex: 0
},
{
PlantType: getRandomElement(plantTypes)!,
EndTime: endTime,
PlotIndex: 1
}
]
}
]
};
};

View File

@ -66,6 +66,18 @@ export const handlePurchase = async (
if (!offer) {
throw new Error(`unknown vendor offer: ${ItemId ? ItemId : purchaseRequest.PurchaseParams.StoreItem}`);
}
if (offer.RegularPrice) {
combineInventoryChanges(
prePurchaseInventoryChanges,
updateCurrency(inventory, offer.RegularPrice[0], false)
);
}
if (offer.PremiumPrice) {
combineInventoryChanges(
prePurchaseInventoryChanges,
updateCurrency(inventory, offer.PremiumPrice[0], true)
);
}
if (offer.ItemPrices) {
handleItemPrices(
inventory,
@ -141,7 +153,8 @@ export const handlePurchase = async (
inventory,
purchaseRequest.PurchaseParams.Quantity,
undefined,
undefined,
false,
purchaseRequest.PurchaseParams.UsePremium,
seed
);
combineInventoryChanges(purchaseResponse.InventoryChanges, prePurchaseInventoryChanges);
@ -169,6 +182,9 @@ export const handlePurchase = async (
purchaseResponse.InventoryChanges,
updateCurrency(inventory, offer.RegularPrice, false)
);
if (purchaseRequest.PurchaseParams.ExpectedPrice) {
throw new Error(`vendor purchase should not have an expected price`);
}
const invItem: IMiscItem = {
ItemType: "/Lotus/Types/Items/MiscItems/PrimeBucks",
@ -222,12 +238,18 @@ export const handlePurchase = async (
const vendor = ExportVendors[purchaseRequest.PurchaseParams.SourceId!];
const offer = vendor.items.find(x => x.storeItem == purchaseRequest.PurchaseParams.StoreItem);
if (offer) {
if (offer.credits) {
if (typeof offer.credits == "number") {
combineInventoryChanges(
purchaseResponse.InventoryChanges,
updateCurrency(inventory, offer.credits, false)
);
}
if (typeof offer.platinum == "number") {
combineInventoryChanges(
purchaseResponse.InventoryChanges,
updateCurrency(inventory, offer.platinum, true)
);
}
if (offer.itemPrices) {
handleItemPrices(
inventory,
@ -238,6 +260,9 @@ export const handlePurchase = async (
}
}
}
if (purchaseRequest.PurchaseParams.ExpectedPrice) {
throw new Error(`vendor purchase should not have an expected price`);
}
break;
case 18: {
if (purchaseRequest.PurchaseParams.SourceId! != worldState.PrimeVaultTraders[0]._id.$oid) {
@ -331,6 +356,7 @@ export const handleStoreItemAcquisition = async (
quantity: number = 1,
durability: TRarity = "COMMON",
ignorePurchaseQuantity: boolean = false,
premiumPurchase: boolean = true,
seed?: bigint
): Promise<IPurchaseResponse> => {
let purchaseResponse = {
@ -352,11 +378,20 @@ export const handleStoreItemAcquisition = async (
}
switch (storeCategory) {
default: {
purchaseResponse = { InventoryChanges: await addItem(inventory, internalName, quantity, true, seed) };
purchaseResponse = {
InventoryChanges: await addItem(inventory, internalName, quantity, premiumPurchase, seed)
};
break;
}
case "Types":
purchaseResponse = await handleTypesPurchase(internalName, inventory, quantity, ignorePurchaseQuantity);
purchaseResponse = await handleTypesPurchase(
internalName,
inventory,
quantity,
ignorePurchaseQuantity,
premiumPurchase,
seed
);
break;
case "Boosters":
purchaseResponse = handleBoostersPurchase(storeItemName, inventory, durability);
@ -478,13 +513,15 @@ const handleTypesPurchase = async (
typesName: string,
inventory: TInventoryDatabaseDocument,
quantity: number,
ignorePurchaseQuantity: boolean
ignorePurchaseQuantity: boolean,
premiumPurchase: boolean = true,
seed?: bigint
): Promise<IPurchaseResponse> => {
const typeCategory = getStoreItemTypesCategory(typesName);
logger.debug(`type category ${typeCategory}`);
switch (typeCategory) {
default:
return { InventoryChanges: await addItem(inventory, typesName, quantity) };
return { InventoryChanges: await addItem(inventory, typesName, quantity, premiumPurchase, seed) };
case "BoosterPacks":
return handleBoosterPackPurchase(typesName, inventory, quantity);
case "SlotItems":

View File

@ -3,18 +3,14 @@ import { isEmptyObject } from "@/src/helpers/general";
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { createMessage } from "@/src/services/inboxService";
import { addItem, addItems, addKeyChainItems, setupKahlSyndicate } from "@/src/services/inventoryService";
import {
fromStoreItem,
getKeyChainMessage,
getLevelKeyRewards,
getQuestCompletionItems
} from "@/src/services/itemDataService";
import { IQuestKeyClient, IQuestKeyDatabase, IQuestStage } from "@/src/types/inventoryTypes/inventoryTypes";
import { fromStoreItem, getKeyChainMessage, getLevelKeyRewards } from "@/src/services/itemDataService";
import { IQuestKeyClient, IQuestKeyDatabase, IQuestStage, ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger";
import { Types } from "mongoose";
import { ExportKeys } from "warframe-public-export-plus";
import { addFixedLevelRewards } from "./missionInventoryUpdateService";
import { IInventoryChanges } from "../types/purchaseTypes";
import questCompletionItems from "@/static/fixed_responses/questCompletionRewards.json";
export interface IUpdateQuestRequest {
QuestKeys: Omit<IQuestKeyDatabase, "CompletionDate">[];
@ -42,23 +38,12 @@ export const updateQuestKey = async (
inventory.QuestKeys[questKeyIndex].overwrite(questKeyUpdate[0]);
let inventoryChanges: IInventoryChanges = {};
const inventoryChanges: IInventoryChanges = {};
if (questKeyUpdate[0].Completed) {
inventory.QuestKeys[questKeyIndex].CompletionDate = new Date();
logger.debug(`completed quest ${questKeyUpdate[0].ItemType} `);
const questKeyName = questKeyUpdate[0].ItemType;
const questCompletionItems = getQuestCompletionItems(questKeyName);
logger.debug(`quest completion items`, questCompletionItems);
if (questCompletionItems) {
inventoryChanges = await addItems(inventory, questCompletionItems);
}
inventory.ActiveQuest = "";
if (questKeyUpdate[0].ItemType == "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain") {
setupKahlSyndicate(inventory);
}
const questKey = questKeyUpdate[0].ItemType;
await handleQuestCompletion(inventory, questKey, inventoryChanges);
}
return inventoryChanges;
};
@ -177,17 +162,124 @@ export const completeQuest = async (inventory: TInventoryDatabaseDocument, quest
await giveKeyChainMissionReward(inventory, { KeyChain: questKey, ChainStage: i });
}
await handleQuestCompletion(inventory, questKey);
};
const getQuestCompletionItems = (questKey: string): ITypeCount[] | undefined => {
if (questKey in questCompletionItems) {
return questCompletionItems[questKey as keyof typeof questCompletionItems];
}
logger.warn(`Quest ${questKey} not found in questCompletionItems`);
const items: ITypeCount[] = [];
const meta = ExportKeys[questKey];
if (meta.rewards) {
for (const reward of meta.rewards) {
if (reward.rewardType == "RT_STORE_ITEM") {
items.push({
ItemType: fromStoreItem(reward.itemType),
ItemCount: 1
});
} else if (reward.rewardType == "RT_RESOURCE" || reward.rewardType == "RT_RECIPE") {
items.push({
ItemType: reward.itemType,
ItemCount: reward.amount
});
}
}
}
return items;
};
// Checks that `questKey` is in `requirements`, and if so, that all other quests in `requirements` are also already completed.
const doesQuestCompletionFinishSet = (
inventory: TInventoryDatabaseDocument,
questKey: string,
requirements: string[]
): boolean => {
let holds = false;
for (const requirement of requirements) {
if (questKey == requirement) {
holds = true;
} else {
if (!inventory.QuestKeys.find(x => x.ItemType == requirement)?.Completed) {
return false;
}
}
}
return holds;
};
const handleQuestCompletion = async (
inventory: TInventoryDatabaseDocument,
questKey: string,
inventoryChanges: IInventoryChanges = {}
): Promise<void> => {
logger.debug(`completed quest ${questKey}`);
if (questKey == "/Lotus/Types/Keys/OrokinMoonQuest/OrokinMoonQuestKeyChain") {
await createMessage(inventory.accountOwnerId, [
{
sndr: "/Lotus/Language/Bosses/Ordis",
msg: "/Lotus/Language/G1Quests/SecondDreamFinishInboxMessage",
att: [
"/Lotus/Weapons/Tenno/Melee/Swords/StalkerTwo/StalkerTwoSmallSword",
"/Lotus/Upgrades/Skins/Sigils/ScarSigil"
],
sub: "/Lotus/Language/G1Quests/SecondDreamFinishInboxTitle",
icon: "/Lotus/Interface/Icons/Npcs/Ordis.png",
highPriority: true
}
]);
} else if (questKey == "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain") {
setupKahlSyndicate(inventory);
}
// Whispers in the Walls is unlocked once The New + Heart of Deimos are completed.
if (
doesQuestCompletionFinishSet(inventory, questKey, [
"/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain",
"/Lotus/Types/Keys/InfestedMicroplanetQuest/InfestedMicroplanetQuestKeyChain"
])
) {
await createMessage(inventory.accountOwnerId, [
{
sndr: "/Lotus/Language/Bosses/Loid",
msg: "/Lotus/Language/EntratiLab/EntratiQuest/WiTWQuestRecievedInboxBody",
att: ["/Lotus/Types/Keys/EntratiLab/EntratiQuestKeyChain"],
sub: "/Lotus/Language/EntratiLab/EntratiQuest/WiTWQuestRecievedInboxTitle",
icon: "/Lotus/Interface/Icons/Npcs/Entrati/Loid.png",
highPriority: true
}
]);
}
// The Hex (Quest) is unlocked once The Lotus Eaters + The Duviri Paradox are completed.
if (
doesQuestCompletionFinishSet(inventory, questKey, [
"/Lotus/Types/Keys/1999PrologueQuest/1999PrologueQuestKeyChain",
"/Lotus/Types/Keys/DuviriQuest/DuviriQuestKeyChain"
])
) {
await createMessage(inventory.accountOwnerId, [
{
sndr: "/Lotus/Language/NewWar/P3M1ChooseMara",
msg: "/Lotus/Language/1999Quest/1999QuestInboxBody",
att: ["/Lotus/Types/Keys/1999Quest/1999QuestKeyChain"],
sub: "/Lotus/Language/1999Quest/1999QuestInboxSubject",
icon: "/Lotus/Interface/Icons/Npcs/Operator.png",
highPriority: true
}
]);
}
const questCompletionItems = getQuestCompletionItems(questKey);
logger.debug(`quest completion items`, questCompletionItems);
if (questCompletionItems) {
await addItems(inventory, questCompletionItems);
await addItems(inventory, questCompletionItems, inventoryChanges);
}
if (inventory.ActiveQuest == questKey) inventory.ActiveQuest = "";
if (questKey == "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain") {
setupKahlSyndicate(inventory);
}
};
export const giveKeyChainItem = async (
@ -197,7 +289,7 @@ export const giveKeyChainItem = async (
const inventoryChanges = await addKeyChainItems(inventory, keyChainInfo);
if (isEmptyObject(inventoryChanges)) {
throw new Error("inventory changes was empty after getting keychain items: should not happen");
logger.warn("inventory changes was empty after getting keychain items: should not happen");
}
// items were added: update quest stage's i (item was given)
updateQuestStage(inventory, keyChainInfo, { i: true });

View File

@ -6,7 +6,7 @@ export interface IRngResult {
probability: number;
}
export const getRandomElement = <T>(arr: T[]): T => {
export const getRandomElement = <T>(arr: T[]): T | undefined => {
return arr[Math.floor(Math.random() * arr.length)];
};
@ -18,7 +18,10 @@ export const getRandomInt = (min: number, max: number): number => {
return Math.floor(Math.random() * (max - min + 1)) + min;
};
const getRewardAtPercentage = <T extends { probability: number }>(pool: T[], percentage: number): T | undefined => {
export const getRewardAtPercentage = <T extends { probability: number }>(
pool: T[],
percentage: number
): T | undefined => {
if (pool.length == 0) return;
const totalChance = pool.reduce((accum, item) => accum + item.probability, 0);
@ -31,7 +34,7 @@ const getRewardAtPercentage = <T extends { probability: number }>(pool: T[], per
return item;
}
}
throw new Error("What the fuck?");
return pool[pool.length - 1];
};
export const getRandomReward = <T extends { probability: number }>(pool: T[]): T | undefined => {
@ -97,18 +100,32 @@ export class CRng {
}
randomInt(min: number, max: number): number {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(this.random() * (max - min + 1)) + min;
const diff = max - min;
if (diff != 0) {
if (diff < 0) {
throw new Error(`max must be greater than min`);
}
if (diff > 0x3fffffff) {
throw new Error(`insufficient entropy`);
}
min += Math.floor(this.random() * (diff + 1));
}
return min;
}
randomElement<T>(arr: T[]): T {
randomElement<T>(arr: T[]): T | undefined {
return arr[Math.floor(this.random() * arr.length)];
}
randomReward<T extends { probability: number }>(pool: T[]): T | undefined {
return getRewardAtPercentage(pool, this.random());
}
churnSeed(its: number): void {
while (its--) {
this.state = (this.state * 1103515245 + 12345) & 0x7fffffff;
}
}
}
// Seeded RNG for cases where we need identical results to the game client. Based on work by Donald Knuth.
@ -128,7 +145,7 @@ export class SRng {
return min;
}
randomElement<T>(arr: T[]): T {
randomElement<T>(arr: T[]): T | undefined {
return arr[this.randomInt(0, arr.length - 1)];
}
@ -136,4 +153,8 @@ export class SRng {
this.state = (0x5851f42d4c957f2dn * this.state + 0x14057b7ef767814fn) & 0xffffffffffffffffn;
return (Number(this.state >> 38n) & 0xffffff) * 0.000000059604645;
}
randomReward<T extends { probability: number }>(pool: T[]): T | undefined {
return getRewardAtPercentage(pool, this.randomFloat());
}
}

View File

@ -13,6 +13,8 @@ import { Types } from "mongoose";
import { isEmptyObject } from "@/src/helpers/general";
import { logger } from "@/src/utils/logger";
import { equipmentKeys, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { IItemConfig } from "../types/inventoryTypes/commonInventoryTypes";
import { importCrewMemberId } from "./importService";
//TODO: setup default items on account creation or like originally in giveStartingItems.php
@ -140,10 +142,30 @@ export const handleInventoryItemConfigChange = async (
case "WeaponSkins": {
const itemEntries = equipment as IItemEntry;
for (const [itemId, itemConfigEntries] of Object.entries(itemEntries)) {
inventory.WeaponSkins.id(itemId)!.IsNew = itemConfigEntries.IsNew;
if (itemId.startsWith("ca70ca70ca70ca70")) {
logger.warn(
`unlockAllSkins does not work with favoriting items because you don't actually own it`
);
} else {
const inventoryItem = inventory.WeaponSkins.id(itemId);
if (!inventoryItem) {
throw new Error(`inventory item WeaponSkins not found with id ${itemId}`);
}
if ("Favorite" in itemConfigEntries) {
inventoryItem.Favorite = itemConfigEntries.Favorite;
}
if ("IsNew" in itemConfigEntries) {
inventoryItem.IsNew = itemConfigEntries.IsNew;
}
}
}
break;
}
case "LotusCustomization": {
logger.debug(`saved LotusCustomization`, equipmentChanges.LotusCustomization);
inventory.LotusCustomization = equipmentChanges.LotusCustomization;
break;
}
default: {
if (equipmentKeys.includes(equipmentName as TEquipmentKey) && equipmentName != "ValidNewLoadoutId") {
logger.debug(`general Item config saved of type ${equipmentName}`, {
@ -159,13 +181,36 @@ export const handleInventoryItemConfigChange = async (
}
for (const [configId, config] of Object.entries(itemConfigEntries)) {
if (typeof config !== "boolean") {
inventoryItem.Configs[parseInt(configId)] = config;
if (/^[0-9]+$/.test(configId)) {
inventoryItem.Configs[parseInt(configId)] = config as IItemConfig;
}
}
if ("Favorite" in itemConfigEntries) {
inventoryItem.Favorite = itemConfigEntries.Favorite;
}
if ("IsNew" in itemConfigEntries) {
inventoryItem.IsNew = itemConfigEntries.IsNew;
}
if ("ItemName" in itemConfigEntries) {
inventoryItem.ItemName = itemConfigEntries.ItemName;
}
if ("RailjackImage" in itemConfigEntries) {
inventoryItem.RailjackImage = itemConfigEntries.RailjackImage;
}
if ("Customization" in itemConfigEntries) {
inventoryItem.Customization = itemConfigEntries.Customization;
}
if ("Weapon" in itemConfigEntries) {
inventoryItem.Weapon = itemConfigEntries.Weapon;
}
if (itemConfigEntries.CrewMembers) {
inventoryItem.CrewMembers = {
SLOT_A: importCrewMemberId(itemConfigEntries.CrewMembers.SLOT_A ?? {}),
SLOT_B: importCrewMemberId(itemConfigEntries.CrewMembers.SLOT_B ?? {}),
SLOT_C: importCrewMemberId(itemConfigEntries.CrewMembers.SLOT_C ?? {})
};
}
}
break;
} else {

View File

@ -1,143 +1,308 @@
import fs from "fs";
import path from "path";
import { repoDir } from "@/src/helpers/pathHelper";
import { unixTimesInMs } from "@/src/constants/timeConstants";
import { catBreadHash } from "@/src/helpers/stringHelpers";
import { CRng, mixSeeds } from "@/src/services/rngService";
import { IMongoDate } from "@/src/types/commonTypes";
import { IItemManifestPreprocessed, IRawVendorManifest, IVendorManifestPreprocessed } from "@/src/types/vendorTypes";
import { JSONParse } from "json-with-bigint";
import { ExportVendors } from "warframe-public-export-plus";
import { IItemManifest, IVendorInfo, IVendorManifest } from "@/src/types/vendorTypes";
import { ExportVendors, IRange } from "warframe-public-export-plus";
const getVendorManifestJson = (name: string): IRawVendorManifest => {
return JSONParse(fs.readFileSync(path.join(repoDir, `static/fixed_responses/getVendorInfo/${name}.json`), "utf-8"));
};
import ArchimedeanVendorManifest from "@/static/fixed_responses/getVendorInfo/ArchimedeanVendorManifest.json";
import DeimosEntratiFragmentVendorProductsManifest from "@/static/fixed_responses/getVendorInfo/DeimosEntratiFragmentVendorProductsManifest.json";
import DeimosHivemindCommisionsManifestFishmonger from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestFishmonger.json";
import DeimosHivemindCommisionsManifestPetVendor from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestPetVendor.json";
import DeimosHivemindCommisionsManifestProspector from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestProspector.json";
import DeimosHivemindCommisionsManifestTokenVendor from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestTokenVendor.json";
import DeimosHivemindCommisionsManifestWeaponsmith from "@/static/fixed_responses/getVendorInfo/DeimosHivemindCommisionsManifestWeaponsmith.json";
import DeimosHivemindTokenVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosHivemindTokenVendorManifest.json";
import DeimosPetVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosPetVendorManifest.json";
import DeimosProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosProspectorVendorManifest.json";
import DuviriAcrithisVendorManifest from "@/static/fixed_responses/getVendorInfo/DuviriAcrithisVendorManifest.json";
import EntratiLabsEntratiLabsCommisionsManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabsCommisionsManifest.json";
import EntratiLabsEntratiLabVendorManifest from "@/static/fixed_responses/getVendorInfo/EntratiLabsEntratiLabVendorManifest.json";
import HubsIronwakeDondaVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsIronwakeDondaVendorManifest.json";
import HubsRailjackCrewMemberVendorManifest from "@/static/fixed_responses/getVendorInfo/HubsRailjackCrewMemberVendorManifest.json";
import MaskSalesmanManifest from "@/static/fixed_responses/getVendorInfo/MaskSalesmanManifest.json";
import Nova1999ConquestShopManifest from "@/static/fixed_responses/getVendorInfo/Nova1999ConquestShopManifest.json";
import OstronPetVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronPetVendorManifest.json";
import OstronProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronProspectorVendorManifest.json";
import RadioLegionIntermission12VendorManifest from "@/static/fixed_responses/getVendorInfo/RadioLegionIntermission12VendorManifest.json";
import SolarisDebtTokenVendorRepossessionsManifest from "@/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorRepossessionsManifest.json";
import SolarisProspectorVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisProspectorVendorManifest.json";
import Temple1999VendorManifest from "@/static/fixed_responses/getVendorInfo/Temple1999VendorManifest.json";
import TeshinHardModeVendorManifest from "@/static/fixed_responses/getVendorInfo/TeshinHardModeVendorManifest.json";
import ZarimanCommisionsManifestArchimedean from "@/static/fixed_responses/getVendorInfo/ZarimanCommisionsManifestArchimedean.json";
const rawVendorManifests: IRawVendorManifest[] = [
getVendorManifestJson("ArchimedeanVendorManifest"),
getVendorManifestJson("DeimosEntratiFragmentVendorProductsManifest"),
getVendorManifestJson("DeimosFishmongerVendorManifest"),
getVendorManifestJson("DeimosHivemindCommisionsManifestFishmonger"),
getVendorManifestJson("DeimosHivemindCommisionsManifestPetVendor"),
getVendorManifestJson("DeimosHivemindCommisionsManifestProspector"),
getVendorManifestJson("DeimosHivemindCommisionsManifestTokenVendor"),
getVendorManifestJson("DeimosHivemindCommisionsManifestWeaponsmith"),
getVendorManifestJson("DeimosHivemindTokenVendorManifest"),
getVendorManifestJson("DeimosPetVendorManifest"),
getVendorManifestJson("DeimosProspectorVendorManifest"),
getVendorManifestJson("DuviriAcrithisVendorManifest"),
getVendorManifestJson("EntratiLabsEntratiLabsCommisionsManifest"),
getVendorManifestJson("EntratiLabsEntratiLabVendorManifest"),
getVendorManifestJson("GuildAdvertisementVendorManifest"), // uses preprocessing
getVendorManifestJson("HubsIronwakeDondaVendorManifest"), // uses preprocessing
getVendorManifestJson("HubsPerrinSequenceWeaponVendorManifest"),
getVendorManifestJson("HubsRailjackCrewMemberVendorManifest"),
getVendorManifestJson("MaskSalesmanManifest"),
getVendorManifestJson("Nova1999ConquestShopManifest"),
getVendorManifestJson("OstronFishmongerVendorManifest"),
getVendorManifestJson("OstronPetVendorManifest"),
getVendorManifestJson("OstronProspectorVendorManifest"),
getVendorManifestJson("RadioLegionIntermission12VendorManifest"),
getVendorManifestJson("SolarisDebtTokenVendorManifest"),
getVendorManifestJson("SolarisDebtTokenVendorRepossessionsManifest"),
getVendorManifestJson("SolarisFishmongerVendorManifest"),
getVendorManifestJson("SolarisProspectorVendorManifest"),
getVendorManifestJson("TeshinHardModeVendorManifest"), // uses preprocessing
getVendorManifestJson("ZarimanCommisionsManifestArchimedean")
const rawVendorManifests: IVendorManifest[] = [
ArchimedeanVendorManifest,
DeimosEntratiFragmentVendorProductsManifest,
DeimosHivemindCommisionsManifestFishmonger,
DeimosHivemindCommisionsManifestPetVendor,
DeimosHivemindCommisionsManifestProspector,
DeimosHivemindCommisionsManifestTokenVendor,
DeimosHivemindCommisionsManifestWeaponsmith,
DeimosHivemindTokenVendorManifest,
DeimosPetVendorManifest,
DeimosProspectorVendorManifest,
DuviriAcrithisVendorManifest,
EntratiLabsEntratiLabsCommisionsManifest,
EntratiLabsEntratiLabVendorManifest,
HubsIronwakeDondaVendorManifest, // uses preprocessing
HubsRailjackCrewMemberVendorManifest,
MaskSalesmanManifest,
Nova1999ConquestShopManifest,
OstronPetVendorManifest,
OstronProspectorVendorManifest,
RadioLegionIntermission12VendorManifest,
SolarisDebtTokenVendorRepossessionsManifest,
SolarisProspectorVendorManifest,
Temple1999VendorManifest,
TeshinHardModeVendorManifest, // uses preprocessing
ZarimanCommisionsManifestArchimedean
];
export const getVendorManifestByTypeName = (typeName: string): IVendorManifestPreprocessed | undefined => {
interface IGeneratableVendorInfo extends Omit<IVendorInfo, "ItemManifest" | "Expiry"> {
cycleOffset?: number;
cycleDuration: number;
}
const generatableVendors: IGeneratableVendorInfo[] = [
{
_id: { $oid: "67dadc30e4b6e0e5979c8d84" },
TypeName: "/Lotus/Types/Game/VendorManifests/TheHex/InfestedLichWeaponVendorManifest",
RandomSeedType: "VRST_WEAPON",
RequiredGoalTag: "",
WeaponUpgradeValueAttenuationExponent: 2.25,
cycleOffset: 1740960000_000,
cycleDuration: 4 * unixTimesInMs.day
},
{
_id: { $oid: "60ad3b6ec96976e97d227e19" },
TypeName: "/Lotus/Types/Game/VendorManifests/Hubs/PerrinSequenceWeaponVendorManifest",
RandomSeedType: "VRST_WEAPON",
WeaponUpgradeValueAttenuationExponent: 2.25,
cycleOffset: 1744934400_000,
cycleDuration: 4 * unixTimesInMs.day
},
{
_id: { $oid: "61ba123467e5d37975aeeb03" },
TypeName: "/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest",
RandomSeedType: "VRST_FLAVOUR_TEXT",
cycleDuration: unixTimesInMs.week // TODO: Auto-detect this based on the items, so we don't need to specify it explicitly.
}
// {
// _id: { $oid: "5dbb4c41e966f7886c3ce939" },
// TypeName: "/Lotus/Types/Game/VendorManifests/Hubs/IronwakeDondaVendorManifest"
// }
];
const getVendorOid = (typeName: string): string => {
return "5be4a159b144f3cd" + catBreadHash(typeName).toString(16).padStart(8, "0");
};
export const getVendorManifestByTypeName = (typeName: string): IVendorManifest | undefined => {
for (const vendorManifest of rawVendorManifests) {
if (vendorManifest.VendorInfo.TypeName == typeName) {
return preprocessVendorManifest(vendorManifest);
}
}
if (typeName == "/Lotus/Types/Game/VendorManifests/TheHex/InfestedLichWeaponVendorManifest") {
return generateCodaWeaponVendorManifest();
for (const vendorInfo of generatableVendors) {
if (vendorInfo.TypeName == typeName) {
return generateVendorManifest(vendorInfo);
}
}
if (typeName in ExportVendors) {
return generateVendorManifest({
_id: { $oid: getVendorOid(typeName) },
TypeName: typeName,
RandomSeedType: ExportVendors[typeName].randomSeedType,
cycleDuration: unixTimesInMs.hour
});
}
return undefined;
};
export const getVendorManifestByOid = (oid: string): IVendorManifestPreprocessed | undefined => {
export const getVendorManifestByOid = (oid: string): IVendorManifest | undefined => {
for (const vendorManifest of rawVendorManifests) {
if (vendorManifest.VendorInfo._id.$oid == oid) {
return preprocessVendorManifest(vendorManifest);
}
}
if (oid == "67dadc30e4b6e0e5979c8d84") {
return generateCodaWeaponVendorManifest();
for (const vendorInfo of generatableVendors) {
if (vendorInfo._id.$oid == oid) {
return generateVendorManifest(vendorInfo);
}
}
for (const [typeName, manifest] of Object.entries(ExportVendors)) {
const typeNameOid = getVendorOid(typeName);
if (typeNameOid == oid) {
return generateVendorManifest({
_id: { $oid: typeNameOid },
TypeName: typeName,
RandomSeedType: manifest.randomSeedType,
cycleDuration: unixTimesInMs.hour
});
}
}
return undefined;
};
const preprocessVendorManifest = (originalManifest: IRawVendorManifest): IVendorManifestPreprocessed => {
const preprocessVendorManifest = (originalManifest: IVendorManifest): IVendorManifest => {
if (Date.now() >= parseInt(originalManifest.VendorInfo.Expiry.$date.$numberLong)) {
const manifest = structuredClone(originalManifest);
const info = manifest.VendorInfo;
refreshExpiry(info.Expiry);
for (const offer of info.ItemManifest) {
const iteration = refreshExpiry(offer.Expiry);
if (offer.ItemPrices) {
for (const price of offer.ItemPrices) {
if (typeof price.ItemType != "string") {
const itemSeed = parseInt(offer.Id.$oid.substring(16), 16);
const rng = new CRng(mixSeeds(itemSeed, iteration));
price.ItemType = rng.randomElement(price.ItemType);
}
}
}
refreshExpiry(offer.Expiry);
}
return manifest as IVendorManifestPreprocessed;
return manifest;
}
return originalManifest as IVendorManifestPreprocessed;
return originalManifest;
};
const refreshExpiry = (expiry: IMongoDate): number => {
const refreshExpiry = (expiry: IMongoDate): void => {
const period = parseInt(expiry.$date.$numberLong);
if (Date.now() >= period) {
const epoch = 1734307200 * 1000; // Monday (for weekly schedules)
const epoch = 1734307200_000; // Monday (for weekly schedules)
const iteration = Math.trunc((Date.now() - epoch) / period);
const start = epoch + iteration * period;
const end = start + period;
expiry.$date.$numberLong = end.toString();
return iteration;
}
return 0;
};
const generateCodaWeaponVendorManifest = (): IVendorManifestPreprocessed => {
const EPOCH = 1740960000 * 1000;
const DUR = 4 * 86400 * 1000;
const cycle = Math.trunc((Date.now() - EPOCH) / DUR);
const cycleStart = EPOCH + cycle * DUR;
const cycleEnd = cycleStart + DUR;
const binThisCycle = cycle % 2; // isOneBinPerCycle
const items: IItemManifestPreprocessed[] = [];
const manifest = ExportVendors["/Lotus/Types/Game/VendorManifests/TheHex/InfestedLichWeaponVendorManifest"];
const rng = new CRng(cycle);
for (const rawItem of manifest.items) {
if (rawItem.bin != binThisCycle) {
continue;
const toRange = (value: IRange | number): IRange => {
if (typeof value == "number") {
return { minValue: value, maxValue: value };
}
return value;
};
const vendorInfoCache: Record<string, IVendorInfo> = {};
const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorManifest => {
if (!(vendorInfo.TypeName in vendorInfoCache)) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { cycleOffset, cycleDuration, ...clientVendorInfo } = vendorInfo;
vendorInfoCache[vendorInfo.TypeName] = {
...clientVendorInfo,
ItemManifest: [],
Expiry: { $date: { $numberLong: "0" } }
};
}
const processed = vendorInfoCache[vendorInfo.TypeName];
if (Date.now() >= parseInt(processed.Expiry.$date.$numberLong)) {
// Remove expired offers
for (let i = 0; i != processed.ItemManifest.length; ) {
if (Date.now() >= parseInt(processed.ItemManifest[i].Expiry.$date.$numberLong)) {
processed.ItemManifest.splice(i, 1);
} else {
++i;
}
}
items.push({
StoreItem: rawItem.storeItem,
ItemPrices: rawItem.itemPrices!.map(item => ({ ...item, ProductCategory: "MiscItems" })),
Bin: "BIN_" + rawItem.bin,
QuantityMultiplier: 1,
Expiry: { $date: { $numberLong: cycleEnd.toString() } },
AllowMultipurchase: false,
LocTagRandSeed: (BigInt(rng.randomInt(0, 0xffffffff)) << 32n) | BigInt(rng.randomInt(0, 0xffffffff)),
Id: { $oid: "67e9da12793a120d" + rng.randomInt(0, 0xffffffff).toString(16).padStart(8, "0") }
});
// Add new offers
const vendorSeed = parseInt(vendorInfo._id.$oid.substring(16), 16);
const cycleOffset = vendorInfo.cycleOffset ?? 1734307200_000;
const cycleDuration = vendorInfo.cycleDuration;
const cycleIndex = Math.trunc((Date.now() - cycleOffset) / cycleDuration);
const rng = new CRng(mixSeeds(vendorSeed, cycleIndex));
const manifest = ExportVendors[vendorInfo.TypeName];
const offersToAdd = [];
if (manifest.numItems && !manifest.isOneBinPerCycle) {
const numItemsTarget = rng.randomInt(manifest.numItems.minValue, manifest.numItems.maxValue);
while (processed.ItemManifest.length + offersToAdd.length < numItemsTarget) {
// TODO: Consider per-bin item limits
// TODO: Consider item probability weightings
offersToAdd.push(rng.randomElement(manifest.items)!);
}
} else {
let binThisCycle;
if (manifest.isOneBinPerCycle) {
binThisCycle = cycleIndex % 2; // Note: May want to auto-compute the bin size, but this is only used for coda weapons right now.
}
for (const rawItem of manifest.items) {
if (!manifest.isOneBinPerCycle || rawItem.bin == binThisCycle) {
offersToAdd.push(rawItem);
}
}
// For most vendors, the offers seem to roughly be in reverse order from the manifest. Coda weapons are an odd exception.
if (!manifest.isOneBinPerCycle) {
offersToAdd.reverse();
}
}
const cycleStart = cycleOffset + cycleIndex * cycleDuration;
for (const rawItem of offersToAdd) {
const durationHoursRange = toRange(rawItem.durationHours);
const expiry =
cycleStart +
rng.randomInt(durationHoursRange.minValue, durationHoursRange.maxValue) * unixTimesInMs.hour;
const item: IItemManifest = {
StoreItem: rawItem.storeItem,
ItemPrices: rawItem.itemPrices?.map(itemPrice => ({ ...itemPrice, ProductCategory: "MiscItems" })),
Bin: "BIN_" + rawItem.bin,
QuantityMultiplier: 1,
Expiry: { $date: { $numberLong: expiry.toString() } },
AllowMultipurchase: false,
Id: {
$oid:
((cycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") +
vendorInfo._id.$oid.substring(8, 16) +
rng.randomInt(0, 0xffff).toString(16).padStart(4, "0") +
rng.randomInt(0, 0xffff).toString(16).padStart(4, "0")
}
};
if (rawItem.numRandomItemPrices) {
item.ItemPrices = [];
for (let i = 0; i != rawItem.numRandomItemPrices; ++i) {
let itemPrice: { type: string; count: IRange };
do {
itemPrice = rng.randomElement(manifest.randomItemPricesPerBin![rawItem.bin])!;
} while (item.ItemPrices.find(x => x.ItemType == itemPrice.type));
item.ItemPrices.push({
ItemType: itemPrice.type,
ItemCount: rng.randomInt(itemPrice.count.minValue, itemPrice.count.maxValue),
ProductCategory: "MiscItems"
});
}
}
if (rawItem.credits) {
const value =
typeof rawItem.credits == "number"
? rawItem.credits
: rng.randomInt(
rawItem.credits.minValue / rawItem.credits.step,
rawItem.credits.maxValue / rawItem.credits.step
) * rawItem.credits.step;
item.RegularPrice = [value, value];
}
if (rawItem.platinum) {
const value =
typeof rawItem.platinum == "number"
? rawItem.platinum
: rng.randomInt(rawItem.platinum.minValue, rawItem.platinum.maxValue);
item.PremiumPrice = [value, value];
}
if (vendorInfo.RandomSeedType) {
item.LocTagRandSeed = (rng.randomInt(0, 0xffff) << 16) | rng.randomInt(0, 0xffff);
if (vendorInfo.RandomSeedType == "VRST_WEAPON") {
const highDword = (rng.randomInt(0, 0xffff) << 16) | rng.randomInt(0, 0xffff);
item.LocTagRandSeed = (BigInt(highDword) << 32n) | (BigInt(item.LocTagRandSeed) & 0xffffffffn);
}
}
processed.ItemManifest.push(item);
}
// Update vendor expiry
let soonestOfferExpiry: number = Number.MAX_SAFE_INTEGER;
for (const offer of processed.ItemManifest) {
const offerExpiry = parseInt(offer.Expiry.$date.$numberLong);
if (soonestOfferExpiry > offerExpiry) {
soonestOfferExpiry = offerExpiry;
}
}
processed.Expiry.$date.$numberLong = soonestOfferExpiry.toString();
}
return {
VendorInfo: {
_id: { $oid: "67dadc30e4b6e0e5979c8d84" },
TypeName: "/Lotus/Types/Game/VendorManifests/TheHex/InfestedLichWeaponVendorManifest",
ItemManifest: items,
PropertyTextHash: "77093DD05A8561A022DEC9A4B9BB4A56",
RandomSeedType: "VRST_WEAPON",
RequiredGoalTag: "",
WeaponUpgradeValueAttenuationExponent: 2.25,
Expiry: { $date: { $numberLong: cycleEnd.toString() } }
}
VendorInfo: processed
};
};

View File

@ -1,6 +1,5 @@
import { Stats, TStatsDatabaseDocument } from "@/src/models/statsModel";
import {
IEnemy,
IStatsAdd,
IStatsMax,
IStatsSet,
@ -137,34 +136,34 @@ export const updateStats = async (accountOwnerId: string, payload: IStatsUpdate)
case "HEADSHOT":
case "KILL_ASSIST": {
playerStats.Enemies ??= [];
const enemyStatKey = {
KILL_ENEMY: "kills",
EXECUTE_ENEMY: "executions",
HEADSHOT: "headshots",
KILL_ASSIST: "assists"
}[category] as "kills" | "executions" | "headshots" | "assists";
const enemyStatKey = (
{
KILL_ENEMY: "kills",
EXECUTE_ENEMY: "executions",
HEADSHOT: "headshots",
KILL_ASSIST: "assists"
} as const
)[category];
for (const [type, count] of Object.entries(data as IUploadEntry)) {
const enemy = playerStats.Enemies.find(element => element.type === type);
if (enemy) {
if (category === "KILL_ENEMY") {
enemy.kills ??= 0;
const captureCount = (actionData as IStatsAdd)["CAPTURE_ENEMY"]?.[type];
if (captureCount) {
enemy.kills += Math.max(count - captureCount, 0);
enemy.captures ??= 0;
enemy.captures += captureCount;
} else {
enemy.kills += count;
}
let enemy = playerStats.Enemies.find(element => element.type === type);
if (!enemy) {
enemy = { type: type };
playerStats.Enemies.push(enemy);
}
if (category === "KILL_ENEMY") {
enemy.kills ??= 0;
const captureCount = (actionData as IStatsAdd)["CAPTURE_ENEMY"]?.[type];
if (captureCount) {
enemy.kills += Math.max(count - captureCount, 0);
enemy.captures ??= 0;
enemy.captures += captureCount;
} else {
enemy[enemyStatKey] ??= 0;
enemy[enemyStatKey] += count;
enemy.kills += count;
}
} else {
const newEnemy: IEnemy = { type: type };
newEnemy[enemyStatKey] = count;
playerStats.Enemies.push(newEnemy);
enemy[enemyStatKey] ??= 0;
enemy[enemyStatKey] += count;
}
}
break;
@ -378,7 +377,8 @@ export const updateStats = async (accountOwnerId: string, payload: IStatsUpdate)
category,
accountOwnerId,
payload.displayName,
data as number
data as number,
payload.guildId
);
break;

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