Compare commits

...

87 Commits
main ... main

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
84 changed files with 3490 additions and 1545 deletions

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,
@ -37,6 +38,7 @@
"noKimCooldowns": false,
"instantResourceExtractorDrones": false,
"noResourceExtractorDronesDamage": false,
"skipClanKeyCrafting": false,
"noDojoRoomBuildStage": false,
"noDecoBuildStage": false,
"fastDojoRoomDestruction": false,

8
package-lock.json generated
View File

@ -18,7 +18,7 @@
"morgan": "^1.10.0",
"ncp": "^2.0.0",
"typescript": "^5.5",
"warframe-public-export-plus": "^0.5.56",
"warframe-public-export-plus": "^0.5.59",
"warframe-riven-info": "^0.1.2",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"
@ -3789,9 +3789,9 @@
}
},
"node_modules/warframe-public-export-plus": {
"version": "0.5.56",
"resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.56.tgz",
"integrity": "sha512-px+J7tUm6fkSzwKkvL73ySQReDq9oM1UrHSLM3vbYGBvELM892iBgPYG45okIhScCSdwmmXTiWZTf4x/I4qiNQ=="
"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

@ -25,7 +25,7 @@
"morgan": "^1.10.0",
"ncp": "^2.0.0",
"typescript": "^5.5",
"warframe-public-export-plus": "^0.5.56",
"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

@ -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

@ -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 =

View File

@ -133,7 +133,14 @@ 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();

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

@ -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

@ -2,7 +2,7 @@ import { toMongoDate } from "@/src/helpers/inventoryHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Guild } from "@/src/models/guildModel";
import { checkClanAscensionHasRequiredContributors } from "@/src/services/guildService";
import { getInventory } from "@/src/services/inventoryService";
import { addFusionPoints, getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { RequestHandler } from "express";
import { Types } from "mongoose";
@ -36,7 +36,7 @@ export const contributeGuildClassController: RequestHandler = async (req, res) =
// 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

@ -62,14 +62,7 @@ export const crewShipIdentifySalvageController: RequestHandler = async (req, res
} satisfies IInnateDamageFingerprint)
};
}
addEquipment(
inventory,
"CrewShipSalvagedWeapons",
payload.ItemType,
undefined,
inventoryChanges,
defaultOverwrites
);
addEquipment(inventory, "CrewShipSalvagedWeapons", payload.ItemType, defaultOverwrites, inventoryChanges);
}
inventoryChanges.CrewShipRawSalvage = [

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

@ -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[] = [
const inventory = await getInventory(accountId);
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"
];
const inventory = await getInventory(accountId);
const inventoryChanges = addEquipment(inventory, "OperatorAmps", request.StartingWeaponType, parts);
]
});
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

@ -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");

View File

@ -441,16 +441,9 @@ const finishComponentRepair = (
const inventoryChanges = {
...(category == "CrewShipWeaponSkins"
? addCrewShipWeaponSkin(inventory, salvageItem.ItemType, salvageItem.UpgradeFingerprint)
: addEquipment(
inventory,
category,
salvageItem.ItemType,
undefined,
{},
{
: addEquipment(inventory, category, salvageItem.ItemType, {
UpgradeFingerprint: salvageItem.UpgradeFingerprint
}
)),
})),
...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, false)
};

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;
@ -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

@ -18,10 +18,12 @@ import {
addMiscItems,
allDailyAffiliationKeys,
cleanupInventory,
createLibraryDailyTask
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);
@ -87,7 +89,7 @@ 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 (
@ -96,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));
};
@ -274,7 +287,7 @@ export const getInventoryResponse = async (
}
// 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 ****
@ -62,6 +63,7 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
missionReport.MissionStatus !== "GS_SUCCESS" &&
!(missionReport.RewardInfo?.jobId || missionReport.RewardInfo?.challengeMissionId)
) {
inventory.RewardSeed = generateRewardSeed();
await inventory.save();
const inventoryResponse = await getInventoryResponse(inventory, true);
res.json({
@ -71,9 +73,16 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
return;
}
const { MissionRewards, inventoryChanges, credits, AffiliationMods, SyndicateXPItemReward } =
await addMissionRewards(inventory, missionReport, firstCompletion);
const {
MissionRewards,
inventoryChanges,
credits,
AffiliationMods,
SyndicateXPItemReward,
ConquestCompletedMissionsCount
} = await addMissionRewards(inventory, missionReport, firstCompletion);
inventory.RewardSeed = generateRewardSeed();
await inventory.save();
const inventoryResponse = await getInventoryResponse(inventory, true);
@ -86,8 +95,9 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
...inventoryUpdates,
//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
});
AffiliationMods,
ConquestCompletedMissionsCount
} satisfies IMissionInventoryUpdateResponse);
};
/*

View File

@ -17,7 +17,7 @@ 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, IDefaultUpgrade } from "warframe-public-export-plus";
import { ExportSentinels, ExportWeapons, IDefaultUpgrade } from "warframe-public-export-plus";
import { Status } from "@/src/types/inventoryTypes/inventoryTypes";
interface IModularCraftRequest {
@ -36,7 +36,9 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res)
const inventory = await getInventory(accountId);
let defaultUpgrades: IDefaultUpgrade[] | undefined;
const defaultOverwrites: Partial<IEquipmentDatabase> = {};
const defaultOverwrites: Partial<IEquipmentDatabase> = {
ModularParts: data.Parts
};
const inventoryChanges: IInventoryChanges = {};
if (category == "KubrowPets") {
const traits = {
@ -138,8 +140,20 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res)
} else {
defaultUpgrades = getDefaultUpgrades(data.Parts);
}
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, data.Parts, inventoryChanges, defaultOverwrites);
addEquipment(inventory, category, data.WeaponType, defaultOverwrites, inventoryChanges);
combineInventoryChanges(
inventoryChanges,
occupySlot(inventory, productCategoryToInventoryBin(category)!, !!data.isWebUi)

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,
{},
{
...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

@ -2,8 +2,12 @@ import {
consumeModCharge,
encodeNemesisGuess,
getInfNodes,
getKnifeUpgrade,
getNemesisPasscode,
IKnifeResponse
getNemesisPasscodeModTypes,
getWeaponsForManifest,
IKnifeResponse,
showdownNodes
} from "@/src/helpers/nemesisHelpers";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
@ -14,6 +18,8 @@ import { IMongoDate, IOid } from "@/src/types/commonTypes";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import {
IInnateDamageFingerprint,
IInventoryClient,
INemesisClient,
InventorySlot,
IUpgradeClient,
IWeaponSkinClient,
@ -99,13 +105,14 @@ export const nemesisController: RequestHandler = async (req, res) => {
encodeNemesisGuess(guess[0], result1, guess[1], result2, guess[2], result3)
);
// Increase antivirus
// 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!;
const response: IKnifeResponse = {};
for (const upgrade of body.knife!.AttachedUpgrades) {
switch (upgrade.ItemType) {
case "/Lotus/Upgrades/Mods/DataSpike/Potency/GainAntivirusAndSpeedOnUseMod":
@ -131,18 +138,12 @@ export const nemesisController: RequestHandler = async (req, res) => {
}
}
inventory.Nemesis!.HenchmenKilled += antivirusGain;
}
if (inventory.Nemesis!.HenchmenKilled >= 100) {
inventory.Nemesis!.HenchmenKilled = 100;
inventory.Nemesis!.InfNodes = [
{
Node: "CrewBattleNode559",
Influence: 1
}
];
inventory.Nemesis!.Weakened = true;
} else {
inventory.Nemesis!.InfNodes = getInfNodes("FC_INFESTATION", 0);
}
await inventory.save();
res.json(response);
@ -170,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 {
@ -223,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)}`);
@ -274,48 +296,19 @@ 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?: {
knife?: IKnife;
}
// interface INemesisWeakenRequest {
// target: INemesisClient;
// knife: IKnife;
// }
interface IKnife {
Item: IEquipmentClient;
Skins: IWeaponSkinClient[];
ModSlot: number;
CustSlot: number;
AttachedUpgrades: IUpgradeClient[];
HiddenWhenHolstered: boolean;
};
}
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"
];

View File

@ -37,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[
@ -45,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
@ -113,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

@ -4,7 +4,6 @@ import { addEmailItem, getInventory, updateCurrency } from "@/src/services/inven
import { getAccountIdForRequest } from "@/src/services/loginService";
import { ICompletedDialogue, IDialogueDatabase } from "@/src/types/inventoryTypes/inventoryTypes";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { logger } from "@/src/utils/logger";
import { RequestHandler } from "express";
export const saveDialogueController: RequestHandler = async (req, res) => {
@ -27,7 +26,6 @@ export const saveDialogueController: RequestHandler = async (req, res) => {
const dialogue = getDialogue(inventory, request.DialogueName);
dialogue.Rank = request.Rank;
dialogue.Chemistry = request.Chemistry;
if (request.Data) {
dialogue.QueuedDialogues = request.QueuedDialogues;
for (const bool of request.Booleans) {
dialogue.Booleans.push(bool);
@ -45,8 +43,6 @@ export const saveDialogueController: RequestHandler = async (req, res) => {
dialogue.Booleans.splice(index, 1);
}
}
dialogue.Completed.push(request.Data);
dialogue.AvailableDate = new Date(tomorrowAt0Utc);
for (const info of request.OtherDialogueInfos) {
const otherDialogue = getDialogue(inventory, info.Dialogue);
if (info.Tag != "") {
@ -54,6 +50,9 @@ export const saveDialogueController: RequestHandler = async (req, res) => {
}
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,
@ -74,7 +73,7 @@ export const saveDialogueController: RequestHandler = async (req, res) => {
AvailableGiftDate: { $date: { $numberLong: tomorrowAt0Utc.toString() } }
});
} else {
logger.error(`saveDialogue request not fully handled: ${String(req.body)}`);
res.end();
}
}
};

View File

@ -8,7 +8,8 @@ import {
addConsumables,
freeUpSlot,
combineInventoryChanges,
addCrewShipRawSalvage
addCrewShipRawSalvage,
addFusionPoints
} from "@/src/services/inventoryService";
import { InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
import { ExportDojoRecipes } from "warframe-public-export-plus";
@ -44,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) {
@ -69,7 +70,7 @@ export const sellController: RequestHandler = async (req, res) => {
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, [
{
@ -147,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 });
@ -280,6 +287,7 @@ interface ISellRequest {
SpaceMelee?: ISellItem[];
Sentinels?: ISellItem[];
SentinelWeapons?: ISellItem[];
MoaPets?: ISellItem[];
OperatorAmps?: ISellItem[];
Hoverboards?: ISellItem[];
Drones?: ISellItem[];

View File

@ -20,7 +20,7 @@ export const setShipFavouriteLoadoutController: RequestHandler = async (req, res
throw new Error(`unexpected BootLocation: ${body.BootLocation}`);
}
await personalRooms.save();
res.json({});
res.json(body);
};
interface ISetShipFavouriteLoadoutRequest {

View File

@ -6,6 +6,9 @@ import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { addMiscItems, combineInventoryChanges, getInventory, updateCurrency } from "@/src/services/inventoryService";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
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);
@ -74,10 +77,14 @@ export const syndicateSacrificeController: RequestHandler = async (request, resp
if (!isStoreItem(rewardType)) {
rewardType = toStoreItem(rewardType);
}
combineInventoryChanges(
res.InventoryChanges,
(await handleStoreItemAcquisition(rewardType, inventory, reward.itemCount)).InventoryChanges
);
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,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;
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

@ -20,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;
@ -32,6 +33,29 @@ interface ListedItem {
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> = {
VPQ_BRONZE: "",
VPQ_SILVER: " [Flawless]",
@ -41,23 +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 = [];
res.KubrowPets = [];
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,
@ -66,7 +95,7 @@ const getItemListsController: RequestHandler = (req, response) => {
});
}
for (const [uniqueName, item] of Object.entries(ExportSentinels)) {
if (item.productCategory != "SpecialItems") {
if (item.productCategory == "Sentinels" || item.productCategory == "KubrowPets") {
res[item.productCategory].push({
uniqueName,
name: getString(item.name, lang),
@ -115,6 +144,7 @@ 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 (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) {
@ -122,6 +152,21 @@ const getItemListsController: RequestHandler = (req, response) => {
} else {
name = name.split("|FISH_SIZE|").join(getString("/Lotus/Language/Fish/FishSizeSmallAbbrev", lang));
}
} else {
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 (
name &&
@ -184,7 +229,6 @@ const getItemListsController: RequestHandler = (req, response) => {
});
}
res.mods = [];
for (const [uniqueName, upgrade] of Object.entries(ExportUpgrades)) {
const mod: ListedItem = {
uniqueName,
@ -242,12 +286,14 @@ const getItemListsController: RequestHandler = (req, response) => {
});
}
}
response.json({
archonCrystalUpgrades,
uniqueLevelCaps: ExportMisc.uniqueLevelCaps,
...res
for (const uniqueName of allIncarnons) {
res.EvolutionProgress.push({
uniqueName,
name: getString(getItemName(uniqueName) || "", lang)
});
}
response.json(res);
};
export { getItemListsController };

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

@ -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,11 +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 } from "../services/inventoryService";
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 = [];
@ -22,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 });
@ -37,17 +40,59 @@ 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 = (nemesis: { fp: bigint; Faction: string }): number[] => {
const rng = new SRng(nemesis.fp);
const passcode = [rng.randomInt(0, 7)];
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") {
passcode.push(rng.randomInt(0, 7));
passcode.push(rng.randomInt(0, 7));
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,
@ -78,6 +123,31 @@ export interface IKnifeResponse {
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,
@ -128,3 +198,181 @@ export const consumeModCharge = (
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,6 +23,7 @@ const dojoDecoSchema = new Schema<IDojoDecoDatabase>({
Type: String,
Pos: [Number],
Rot: [Number],
Scale: Number,
Name: String,
Sockets: Number,
RegularCredits: Number,

View File

@ -96,7 +96,8 @@ import {
IInvasionProgressDatabase,
IInvasionProgressClient,
IAccolades,
IHubNpcCustomization
IHubNpcCustomization,
ILotusCustomization
} from "../../types/inventoryTypes/inventoryTypes";
import { IOid } from "../../types/commonTypes";
import {
@ -390,7 +391,7 @@ MailboxSchema.set("toJSON", {
const DuviriInfoSchema = new Schema<IDuviriInfo>(
{
Seed: Number,
Seed: BigInt,
NumCompletions: { type: Number, default: 0 }
},
{
@ -780,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,
@ -1034,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 },
@ -1125,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 }
@ -1255,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
},
@ -1376,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,
@ -1426,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],
@ -1472,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],
@ -1511,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],
@ -1531,7 +1554,7 @@ 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: [invasionProgressSchema],
FactionScores: [Number],
@ -1566,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,
@ -1599,13 +1622,13 @@ 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,
@ -1619,7 +1642,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
PlayerSkills: { type: playerSkillsSchema, default: {} },
//TradeBannedUntil data
TradeBannedUntil: Schema.Types.Mixed,
//TradeBannedUntil: Schema.Types.Mixed,
//https://warframe.fandom.com/wiki/Helminth
InfestedFoundry: infestedFoundrySchema,
@ -1628,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
@ -1639,23 +1662,24 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
//Unknown and system
DuviriInfo: DuviriInfoSchema,
LastInventorySync: Schema.Types.ObjectId,
Mailbox: MailboxSchema,
HandlerPoints: 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: [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 },
@ -1736,6 +1760,9 @@ inventorySchema.set("toJSON", {
sn: inventoryDatabase.LockedWeaponGroup.sn ? toOid(inventoryDatabase.LockedWeaponGroup.sn) : undefined
};
}
if (inventoryDatabase.LastInventorySync) {
inventoryResponse.LastInventorySync = toOid(inventoryDatabase.LastInventorySync);
}
}
});

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 {
IFavouriteLoadoutDatabase,
IGardening,
IGardeningDatabase,
IPlacedDecosDatabase,
IPictureFrameInfo,
IRoom,
ITailorShopDatabase,
IApartmentDatabase
IApartmentDatabase,
IPlanterDatabase,
IPlantDatabase,
IPlantClient
} from "@/src/types/shipTypes";
import { Schema, model } from "mongoose";
@ -77,15 +80,45 @@ favouriteLoadoutSchema.set("toJSON", {
}
});
const gardeningSchema = new Schema<IGardening>({
Planters: [Schema.Types.Mixed] //TODO: add when implementing gardening
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 // TODO: ensure this is correct
Gardening: gardeningSchema
},
{ _id: false }
);
@ -98,7 +131,9 @@ const apartmentDefault: IApartmentDatabase = {
{ Name: "DuviriHallway", MaxCapacity: 1600 }
],
FavouriteLoadouts: [],
Gardening: {}
Gardening: {
Planters: []
}
};
const orbiterSchema = new Schema<IOrbiter>(

View File

@ -19,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";
@ -47,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";
@ -158,6 +160,7 @@ 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);
@ -238,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);

View File

@ -10,6 +10,7 @@ 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";
@ -17,10 +18,11 @@ import { addCurrencyController } from "@/src/controllers/custom/addCurrencyContr
import { addItemsController } from "@/src/controllers/custom/addItemsController";
import { addXpController } from "@/src/controllers/custom/addXpController";
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();
@ -34,6 +36,7 @@ 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);
@ -42,6 +45,7 @@ customRouter.post("/addItems", addItemsController);
customRouter.post("/addXp", addXpController);
customRouter.post("/import", importController);
customRouter.post("/manageQuests", manageQuestsController);
customRouter.post("/setEvolutionProgress", setEvolutionProgressController);
customRouter.get("/config", getConfigDataController);
customRouter.post("/config", updateConfigDataController);

View File

@ -24,6 +24,7 @@ interface IConfig {
infiniteEndo?: boolean;
infiniteRegalAya?: boolean;
infiniteHelminthMaterials?: boolean;
dontSubtractConsumables?: boolean;
unlockAllShipFeatures?: boolean;
unlockAllShipDecorations?: boolean;
unlockAllFlavourItems?: boolean;
@ -43,6 +44,7 @@ interface IConfig {
noKimCooldowns?: boolean;
instantResourceExtractorDrones?: boolean;
noResourceExtractorDronesDamage?: boolean;
skipClanKeyCrafting?: boolean;
noDojoRoomBuildStage?: boolean;
noDojoDecoBuildStage?: boolean;
fastDojoRoomDestruction?: 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 {
@ -105,6 +105,7 @@ export const getGuildClient = async (guild: TGuildDatabaseDocument, accountId: s
Members: members,
Ranks: guild.Ranks,
Tier: guild.Tier,
Emblem: guild.Emblem,
Vault: getGuildVault(guild),
ActiveDojoColorResearch: guild.ActiveDojoColorResearch,
Class: guild.Class,
@ -133,7 +134,7 @@ export const getGuildVault = (guild: TGuildDatabaseDocument): IGuildVault => {
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() },
@ -222,6 +223,7 @@ 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
@ -503,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"
@ -512,7 +514,7 @@ export const removePigmentsFromGuildMembers = async (guildId: string | Types.Obj
inventory.MiscItems.splice(index, 1);
await inventory.save();
}
}
});
};
export const processGuildTechProjectContributionsUpdate = async (
@ -552,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);
@ -655,6 +657,32 @@ export const checkClanAscensionHasRequiredContributors = async (guild: TGuildDat
}
};
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 => {
const inventoryChanges: 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));
@ -168,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) {
@ -212,20 +229,7 @@ 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]);
}
@ -363,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;

View File

@ -18,7 +18,6 @@ import {
IKubrowPetEggDatabase,
IKubrowPetEggClient,
ILibraryDailyTaskInfo,
ICalendarProgress,
IDroneClient,
IUpgradeClient,
TPartialStartingGear,
@ -26,7 +25,10 @@ import {
ICrewMemberClient,
Status,
IKubrowPetDetailsDatabase,
ITraits
ITraits,
ICalendarProgress,
INemesisWeaponTargetFingerprint,
INemesisPetTargetFingerprint
} from "@/src/types/inventoryTypes/inventoryTypes";
import { IGenericUpdate, IUpdateNodeIntrosResponse } from "../types/genericUpdate";
import { IKeyChainRequest, IMissionInventoryUpdateRequest } from "../types/requestTypes";
@ -78,6 +80,8 @@ import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json";
import { getRandomElement, getRandomInt, getRandomWeightedReward, SRng } from "./rngService";
import { createMessage } from "./inboxService";
import { getMaxStanding } from "@/src/helpers/syndicateStandingHelper";
import { getWorldState } from "./worldStateService";
import { getInnateDamageTag, getInnateDamageValue } from "../helpers/nemesisHelpers";
export const createInventory = async (
accountOwnerId: Types.ObjectId,
@ -91,7 +95,6 @@ export const createInventory = async (
});
inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask();
inventory.CalendarProgress = createCalendar();
inventory.RewardSeed = generateRewardSeed();
inventory.DuviriInfo = {
Seed: generateRewardSeed(),
@ -120,10 +123,15 @@ export const createInventory = async (
}
};
export const generateRewardSeed = (): number => {
const min = -Number.MAX_SAFE_INTEGER;
const max = Number.MAX_SAFE_INTEGER;
return Math.floor(Math.random() * (max - min + 1)) + min;
export const generateRewardSeed = (): bigint => {
const hiDword = getRandomInt(0, 0x7fffffff);
const loDword = getRandomInt(0, 0xffffffff);
let seed = (BigInt(hiDword) << 32n) | BigInt(loDword);
if (Math.random() < 0.5) {
seed *= -1n;
seed -= 1n;
}
return seed;
};
//TODO: RawUpgrades might need to return a LastAdded
@ -138,7 +146,7 @@ const awakeningRewards = [
export const addStartingGear = async (
inventory: TInventoryDatabaseDocument,
startingGear: TPartialStartingGear | undefined = undefined
startingGear?: TPartialStartingGear
): Promise<IInventoryChanges> => {
const { LongGuns, Pistols, Suits, Melee } = startingGear || {
LongGuns: [{ ItemType: "/Lotus/Weapons/Tenno/Rifle/Rifle" }],
@ -149,23 +157,22 @@ export const addStartingGear = async (
//TODO: properly merge weapon bin changes it is currently static here
const inventoryChanges: IInventoryChanges = {};
addEquipment(inventory, "LongGuns", LongGuns[0].ItemType, undefined, inventoryChanges);
addEquipment(inventory, "Pistols", Pistols[0].ItemType, undefined, inventoryChanges);
addEquipment(inventory, "Melee", Melee[0].ItemType, undefined, inventoryChanges);
await addPowerSuit(inventory, Suits[0].ItemType, inventoryChanges);
addEquipment(inventory, "LongGuns", LongGuns[0].ItemType, { IsNew: false }, inventoryChanges);
addEquipment(inventory, "Pistols", Pistols[0].ItemType, { IsNew: false }, inventoryChanges);
addEquipment(inventory, "Melee", Melee[0].ItemType, { IsNew: false }, inventoryChanges);
await addPowerSuit(inventory, Suits[0].ItemType, { IsNew: false }, inventoryChanges);
addEquipment(
inventory,
"DataKnives",
"/Lotus/Weapons/Tenno/HackingDevices/TnHackingDevice/TnHackingDeviceWeapon",
undefined,
inventoryChanges,
{ XP: 450_000 }
{ XP: 450_000, IsNew: false },
inventoryChanges
);
addEquipment(
inventory,
"Scoops",
"/Lotus/Weapons/Tenno/Speedball/SpeedballWeaponTest",
undefined,
{ IsNew: false },
inventoryChanges
);
@ -208,6 +215,15 @@ export const combineInventoryChanges = (InventoryChanges: IInventoryChanges, del
for (const key in delta) {
if (!(key in InventoryChanges)) {
InventoryChanges[key] = delta[key];
} else if (key == "MiscItems") {
for (const deltaItem of delta[key]!) {
const existing = InventoryChanges[key]!.find(x => x.ItemType == deltaItem.ItemType);
if (existing) {
existing.ItemCount += deltaItem.ItemCount;
} else {
InventoryChanges[key]!.push(deltaItem);
}
}
} else if (Array.isArray(delta[key])) {
const left = InventoryChanges[key] as object[];
const right: object[] = delta[key];
@ -240,7 +256,7 @@ export const combineInventoryChanges = (InventoryChanges: IInventoryChanges, del
export const getInventory = async (
accountOwnerId: string,
projection: string | undefined = undefined
projection?: string
): Promise<TInventoryDatabaseDocument> => {
const inventory = await Inventory.findOne({ accountOwnerId: accountOwnerId }, projection);
@ -314,7 +330,8 @@ export const addItem = async (
typeName: string,
quantity: number = 1,
premiumPurchase: boolean = false,
seed?: bigint
seed?: bigint,
targetFingerprint?: string
): Promise<IInventoryChanges> => {
// Bundles are technically StoreItems but a) they don't have a normal counterpart, and b) they are used in non-StoreItem contexts, e.g. email attachments.
if (typeName in ExportBundles) {
@ -517,14 +534,13 @@ export const addItem = async (
]
});
}
const inventoryChanges = addEquipment(
inventory,
weapon.productCategory,
typeName,
[],
{},
defaultOverwrites
);
if (targetFingerprint) {
const targetFingerprintObj = JSON.parse(targetFingerprint) as INemesisWeaponTargetFingerprint;
defaultOverwrites.UpgradeType = targetFingerprintObj.ItemType;
defaultOverwrites.UpgradeFingerprint = JSON.stringify(targetFingerprintObj.UpgradeFingerprint);
defaultOverwrites.ItemName = targetFingerprintObj.Name;
}
const inventoryChanges = addEquipment(inventory, weapon.productCategory, typeName, defaultOverwrites);
if (weapon.additionalItems) {
for (const item of weapon.additionalItems) {
combineInventoryChanges(inventoryChanges, await addItem(inventory, item, 1));
@ -532,7 +548,32 @@ export const addItem = async (
}
return {
...inventoryChanges,
...occupySlot(inventory, InventorySlot.WEAPONS, premiumPurchase)
...occupySlot(
inventory,
productCategoryToInventoryBin(weapon.productCategory) ?? InventorySlot.WEAPONS,
premiumPurchase
)
};
} else if (targetFingerprint) {
// Sister's Hound
const targetFingerprintObj = JSON.parse(targetFingerprint) as INemesisPetTargetFingerprint;
const head = targetFingerprintObj.Parts[0];
const defaultOverwrites: Partial<IEquipmentDatabase> = {
ModularParts: targetFingerprintObj.Parts,
ItemName: targetFingerprintObj.Name,
Configs: applyDefaultUpgrades(inventory, ExportWeapons[head].defaultUpgrades)
};
const 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"
}[head] as string;
return {
...addEquipment(inventory, "MoaPets", itemType, defaultOverwrites),
...occupySlot(inventory, InventorySlot.SENTINELS, premiumPurchase)
};
} else {
// Modular weapon parts
@ -580,7 +621,7 @@ export const addItem = async (
}
if (typeName in ExportFusionBundles) {
const fusionPointsTotal = ExportFusionBundles[typeName].fusionPoints * quantity;
inventory.FusionPoints += fusionPointsTotal;
addFusionPoints(inventory, fusionPointsTotal);
return {
FusionPoints: fusionPointsTotal
};
@ -621,12 +662,9 @@ export const addItem = async (
switch (typeName.substr(1).split("/")[2]) {
default: {
return {
...(await addPowerSuit(
inventory,
typeName,
{},
premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined
)),
...(await addPowerSuit(inventory, typeName, {
Features: premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined
})),
...occupySlot(inventory, InventorySlot.SUITS, premiumPurchase)
};
}
@ -821,7 +859,8 @@ const addSentinel = (
const features = premiumPurchase ? EquipmentFeatures.DOUBLE_CAPACITY : undefined;
const sentinelIndex =
inventory.Sentinels.push({ ItemType: sentinelName, Configs: configs, XP: 0, Features: features }) - 1;
inventory.Sentinels.push({ ItemType: sentinelName, Configs: configs, XP: 0, Features: features, IsNew: true }) -
1;
inventoryChanges.Sentinels ??= [];
inventoryChanges.Sentinels.push(inventory.Sentinels[sentinelIndex].toJSON<IEquipmentClient>());
@ -845,8 +884,8 @@ const addSentinelWeapon = (
export const addPowerSuit = async (
inventory: TInventoryDatabaseDocument,
powersuitName: string,
inventoryChanges: IInventoryChanges = {},
features: number | undefined = undefined
defaultOverwrites?: Partial<IEquipmentDatabase>,
inventoryChanges: IInventoryChanges = {}
): Promise<IInventoryChanges> => {
const powersuit = ExportWarframes[powersuitName] as IPowersuit | undefined;
const exalted = powersuit?.exalted ?? [];
@ -860,15 +899,20 @@ export const addPowerSuit = async (
}
}
}
const suitIndex =
inventory.Suits.push({
const suit: Omit<IEquipmentDatabase, "_id"> = Object.assign(
{
ItemType: powersuitName,
Configs: [],
UpgradeVer: 101,
XP: 0,
Features: features,
IsNew: true
}) - 1;
},
defaultOverwrites
);
if (!suit.IsNew) {
suit.IsNew = undefined;
}
const suitIndex = inventory.Suits.push(suit) - 1;
inventoryChanges.Suits ??= [];
inventoryChanges.Suits.push(inventory.Suits[suitIndex].toJSON<IEquipmentClient>());
return inventoryChanges;
@ -878,7 +922,7 @@ export const addMechSuit = async (
inventory: TInventoryDatabaseDocument,
mechsuitName: string,
inventoryChanges: IInventoryChanges = {},
features: number | undefined = undefined
features?: number
): Promise<IInventoryChanges> => {
const powersuit = ExportWarframes[mechsuitName] as IPowersuit | undefined;
const exalted = powersuit?.exalted ?? [];
@ -930,7 +974,7 @@ export const addSpaceSuit = (
inventory: TInventoryDatabaseDocument,
spacesuitName: string,
inventoryChanges: IInventoryChanges = {},
features: number | undefined = undefined
features?: number
): IInventoryChanges => {
const suitIndex =
inventory.SpaceSuits.push({
@ -1069,6 +1113,15 @@ export const updateCurrency = (
return currencyChanges;
};
export const addFusionPoints = (inventory: TInventoryDatabaseDocument, add: number): number => {
if (inventory.FusionPoints + add > 2147483647) {
logger.warn(`capping FusionPoints balance at 2147483647`);
add = 2147483647 - inventory.FusionPoints;
}
inventory.FusionPoints += add;
return add;
};
const standingLimitBinToInventoryKey: Record<
Exclude<TStandingLimitBin, "STANDING_LIMIT_BIN_NONE">,
keyof IDailyAffiliations
@ -1180,20 +1233,21 @@ export const addEquipment = (
inventory: TInventoryDatabaseDocument,
category: TEquipmentKey,
type: string,
modularParts: string[] | undefined = undefined,
inventoryChanges: IInventoryChanges = {},
defaultOverwrites: Partial<IEquipmentDatabase> | undefined = undefined
defaultOverwrites?: Partial<IEquipmentDatabase>,
inventoryChanges: IInventoryChanges = {}
): IInventoryChanges => {
const equipment = Object.assign(
const equipment: Omit<IEquipmentDatabase, "_id"> = Object.assign(
{
ItemType: type,
Configs: [],
XP: 0,
ModularParts: modularParts,
IsNew: category != "CrewShipWeapons" && category != "CrewShipSalvagedWeapons" ? true : undefined
IsNew: category != "CrewShipWeapons" && category != "CrewShipSalvagedWeapons"
},
defaultOverwrites
);
if (!equipment.IsNew) {
equipment.IsNew = undefined;
}
const index = inventory[category].push(equipment) - 1;
inventoryChanges[category] ??= [];
@ -1222,12 +1276,16 @@ export const addSkin = (
typeName: string,
inventoryChanges: IInventoryChanges = {}
): IInventoryChanges => {
if (inventory.WeaponSkins.find(x => x.ItemType == typeName)) {
logger.debug(`refusing to add WeaponSkin ${typeName} because account already owns it`);
} else {
const index = inventory.WeaponSkins.push({ ItemType: typeName, IsNew: true }) - 1;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
inventoryChanges.WeaponSkins ??= [];
(inventoryChanges.WeaponSkins as IWeaponSkinClient[]).push(
inventory.WeaponSkins[index].toJSON<IWeaponSkinClient>()
);
}
return inventoryChanges;
};
@ -1445,6 +1503,22 @@ export const addGearExpByCategory = (
});
};
export const addMiscItem = (
inventory: TInventoryDatabaseDocument,
type: string,
count: number,
inventoryChanges: IInventoryChanges
): void => {
const miscItemChanges: IMiscItem[] = [
{
ItemType: type,
ItemCount: count
}
];
addMiscItems(inventory, miscItemChanges);
combineInventoryChanges(inventoryChanges, { MiscItems: miscItemChanges });
};
export const addMiscItems = (inventory: TInventoryDatabaseDocument, itemsArray: IMiscItem[]): void => {
const { MiscItems } = inventory;
@ -1724,7 +1798,7 @@ export const addKeyChainItems = async (
};
export const createLibraryDailyTask = (): ILibraryDailyTaskInfo => {
const enemyTypes = getRandomElement(libraryDailyTasks);
const enemyTypes = getRandomElement(libraryDailyTasks)!;
const enemyAvatar = ExportEnemies.avatars[enemyTypes[0]];
const scansRequired = getRandomInt(2, 4);
return {
@ -1738,20 +1812,6 @@ export const createLibraryDailyTask = (): ILibraryDailyTaskInfo => {
};
};
const createCalendar = (): ICalendarProgress => {
return {
Version: 19,
Iteration: 2,
YearProgress: { Upgrades: [] },
SeasonProgress: {
SeasonType: "CST_SPRING",
LastCompletedDayIdx: -1,
LastCompletedChallengeDayIdx: -1,
ActivatedChallenges: []
}
};
};
export const setupKahlSyndicate = (inventory: TInventoryDatabaseDocument): void => {
inventory.Affiliations.push({
Title: 1,
@ -1787,4 +1847,132 @@ export const cleanupInventory = (inventory: TInventoryDatabaseDocument): void =>
logger.debug(`removing FreeFavorsEarned from LibrarySyndicate`);
LibrarySyndicate.FreeFavorsEarned = undefined;
}
if (inventory.LotusCustomization) {
if (
Array.isArray(inventory.LotusCustomization.attcol) ||
Array.isArray(inventory.LotusCustomization.sigcol) ||
Array.isArray(inventory.LotusCustomization.eyecol) ||
Array.isArray(inventory.LotusCustomization.facial) ||
Array.isArray(inventory.LotusCustomization.cloth) ||
Array.isArray(inventory.LotusCustomization.syancol)
) {
logger.debug(`fixing empty objects represented as empty arrays in LotusCustomization`);
inventory.LotusCustomization.attcol = {};
inventory.LotusCustomization.sigcol = {};
inventory.LotusCustomization.eyecol = {};
inventory.LotusCustomization.facial = {};
inventory.LotusCustomization.cloth = {};
inventory.LotusCustomization.syancol = {};
}
}
};
export const getCalendarProgress = (inventory: TInventoryDatabaseDocument): ICalendarProgress => {
const currentSeason = getWorldState().KnownCalendarSeasons[0];
if (!inventory.CalendarProgress) {
inventory.CalendarProgress = {
Version: 19,
Iteration: currentSeason.YearIteration,
YearProgress: {
Upgrades: []
},
SeasonProgress: {
SeasonType: currentSeason.Season,
LastCompletedDayIdx: 0,
LastCompletedChallengeDayIdx: 0,
ActivatedChallenges: []
}
};
}
const yearRolledOver = inventory.CalendarProgress.Iteration != currentSeason.YearIteration;
if (yearRolledOver) {
inventory.CalendarProgress.Iteration = currentSeason.YearIteration;
inventory.CalendarProgress.YearProgress.Upgrades = [];
}
if (yearRolledOver || inventory.CalendarProgress.SeasonProgress.SeasonType != currentSeason.Season) {
inventory.CalendarProgress.SeasonProgress.SeasonType = currentSeason.Season;
inventory.CalendarProgress.SeasonProgress.LastCompletedDayIdx = -1;
inventory.CalendarProgress.SeasonProgress.LastCompletedChallengeDayIdx = -1;
inventory.CalendarProgress.SeasonProgress.ActivatedChallenges = [];
}
return inventory.CalendarProgress;
};
export const giveNemesisWeaponRecipe = (
inventory: TInventoryDatabaseDocument,
weaponType: string,
nemesisName: string = "AGOR ROK",
weaponLoc?: string,
KillingSuit: string = "/Lotus/Powersuits/Ember/Ember",
fp: bigint = generateRewardSeed()
): void => {
if (!weaponLoc) {
weaponLoc = ExportWeapons[weaponType].name;
}
const recipeType = Object.entries(ExportRecipes).find(arr => arr[1].resultType == weaponType)![0];
addRecipes(inventory, [
{
ItemType: recipeType,
ItemCount: 1
}
]);
inventory.PendingRecipes.push({
CompletionDate: new Date(),
ItemType: recipeType,
TargetFingerprint: JSON.stringify({
ItemType: "/Lotus/Weapons/Grineer/KuvaLich/Upgrades/InnateDamageRandomMod",
UpgradeFingerprint: {
compat: weaponType,
buffs: [
{
Tag: getInnateDamageTag(KillingSuit),
Value: getInnateDamageValue(fp)
}
]
},
Name: weaponLoc + "|" + nemesisName
} satisfies INemesisWeaponTargetFingerprint)
});
};
export const giveNemesisPetRecipe = (inventory: TInventoryDatabaseDocument, nemesisName: string = "AGOR ROK"): void => {
const head = getRandomElement([
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadA",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartHeadC"
])!;
const body = getRandomElement([
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyA",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartBodyC"
])!;
const legs = getRandomElement([
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsA",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartLegsC"
])!;
const tail = getRandomElement([
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailA",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailB",
"/Lotus/Types/Friendly/Pets/ZanukaPets/ZanukaPetParts/ZanukaPetPartTailC"
])!;
const recipeType = Object.entries(ExportRecipes).find(arr => arr[1].resultType == head)![0];
addRecipes(inventory, [
{
ItemType: recipeType,
ItemCount: 1
}
]);
inventory.PendingRecipes.push({
CompletionDate: new Date(),
ItemType: recipeType,
TargetFingerprint: JSON.stringify({
Parts: [head, body, legs, tail],
Name: "/Lotus/Language/Pets/ZanukaPetName|" + nemesisName
} satisfies INemesisPetTargetFingerprint)
});
};

View File

@ -17,6 +17,7 @@ import {
dict_uk,
dict_zh,
ExportArcanes,
ExportBoosters,
ExportCustoms,
ExportDrones,
ExportGear,
@ -217,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 => {
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

@ -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()),

View File

@ -19,6 +19,7 @@ import {
addCrewShipRawSalvage,
addEmailItem,
addFocusXpIncreases,
addFusionPoints,
addFusionTreasures,
addGearExpByCategory,
addItem,
@ -29,9 +30,13 @@ import {
addMods,
addRecipes,
addShipDecorations,
addSkin,
addStanding,
combineInventoryChanges,
generateRewardSeed,
getCalendarProgress,
giveNemesisPetRecipe,
giveNemesisWeaponRecipe,
updateCurrency,
updateSyndicate
} from "@/src/services/inventoryService";
@ -43,14 +48,14 @@ import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/invento
import { getEntriesUnsafe } from "@/src/utils/ts-utils";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { handleStoreItemAcquisition } from "./purchaseService";
import { IMissionReward } from "../types/missionTypes";
import { IMissionCredits, IMissionReward } from "../types/missionTypes";
import { crackRelic } from "@/src/helpers/relicHelper";
import { createMessage } from "./inboxService";
import kuriaMessage50 from "@/static/fixed_responses/kuriaMessages/fiftyPercent.json";
import kuriaMessage75 from "@/static/fixed_responses/kuriaMessages/seventyFivePercent.json";
import kuriaMessage100 from "@/static/fixed_responses/kuriaMessages/oneHundredPercent.json";
import conservationAnimals from "@/static/fixed_responses/conservationAnimals.json";
import { getInfNodes } from "@/src/helpers/nemesisHelpers";
import { getInfNodes, getWeaponsForManifest, sendCodaFinishedMessage } from "@/src/helpers/nemesisHelpers";
import { Loadout } from "../models/inventoryModels/loadoutModel";
import { ILoadoutConfigDatabase } from "../types/saveLoadoutTypes";
import { getLiteSortie, getWorldState, idToWeek } from "./worldStateService";
@ -67,18 +72,22 @@ const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[]
return rotations;
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const missionIndex: number | undefined = ExportRegions[rewardInfo.node]?.missionIndex;
// For Rescue missions
if (rewardInfo.node in ExportRegions && ExportRegions[rewardInfo.node].missionIndex == 3 && rewardInfo.rewardTier) {
if (missionIndex == 3 && rewardInfo.rewardTier) {
return [rewardInfo.rewardTier];
}
// Aborting a railjack mission should not give any rewards (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1741)
if (rewardInfo.rewardQualifications === undefined) {
return [];
}
const rotationCount = rewardInfo.rewardQualifications?.length || 0;
const rotationCount = rewardInfo.rewardQualifications.length || 0;
if (rotationCount === 0) return [0];
// Empty or absent rewardQualifications should not give rewards when:
// - Completing only 1 zone of (E)SO (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1823)
// - Aborting a railjack mission (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1741)
if (rotationCount == 0 && missionIndex != 30 && missionIndex != 32) {
return [0];
}
const rotationPattern =
tierOverride === undefined
@ -184,6 +193,12 @@ export const addMissionInventoryUpdates = async (
if (inventoryUpdates.RewardInfo.NemesisAbandonedRewards) {
inventory.NemesisAbandonedRewards = inventoryUpdates.RewardInfo.NemesisAbandonedRewards;
}
if (inventoryUpdates.RewardInfo.NemesisHenchmenKills && inventory.Nemesis) {
inventory.Nemesis.HenchmenKilled += inventoryUpdates.RewardInfo.NemesisHenchmenKills;
}
if (inventoryUpdates.RewardInfo.NemesisHintProgress && inventory.Nemesis) {
inventory.Nemesis.HintProgress += inventoryUpdates.RewardInfo.NemesisHintProgress;
}
if (inventoryUpdates.MissionStatus == "GS_SUCCESS" && inventoryUpdates.RewardInfo.jobId) {
// e.g. for Profit-Taker Phase 1:
// JobTier: -6,
@ -265,7 +280,14 @@ export const addMissionInventoryUpdates = async (
addMiscItems(inventory, value);
break;
case "Consumables":
if (config.dontSubtractConsumables) {
addConsumables(
inventory,
value.filter(x => x.ItemCount > 0)
);
} else {
addConsumables(inventory, value);
}
break;
case "Recipes":
addRecipes(inventory, value);
@ -287,14 +309,14 @@ export const addMissionInventoryUpdates = async (
addShipDecorations(inventory, value);
break;
case "FusionBundles": {
let fusionPoints = 0;
let fusionPointsDelta = 0;
for (const fusionBundle of value) {
const fusionPointsTotal =
ExportFusionBundles[fusionBundle.ItemType].fusionPoints * fusionBundle.ItemCount;
inventory.FusionPoints += fusionPointsTotal;
fusionPoints += fusionPointsTotal;
fusionPointsDelta += addFusionPoints(
inventory,
ExportFusionBundles[fusionBundle.ItemType].fusionPoints * fusionBundle.ItemCount
);
}
inventoryChanges.FusionPoints = fusionPoints;
inventoryChanges.FusionPoints = fusionPointsDelta;
break;
}
case "EmailItems": {
@ -358,6 +380,7 @@ export const addMissionInventoryUpdates = async (
: 10)
) {
progress.Completed = true;
inventory.LibraryPersonalTarget = undefined;
}
logger.debug(`synthesis of ${scan.EnemyType} added to personal target progress`);
synthesisIgnored = false;
@ -411,6 +434,11 @@ export const addMissionInventoryUpdates = async (
upgrade.UpgradeFingerprint = clientUpgrade.UpgradeFingerprint; // primitive way to copy over the riven challenge progress
});
break;
case "WeaponSkins":
for (const item of value) {
addSkin(inventory, item.ItemType);
}
break;
case "Boosters":
value.forEach(booster => {
addBooster(booster.ItemType, booster.ExpiryDate, inventory);
@ -488,6 +516,16 @@ export const addMissionInventoryUpdates = async (
}
break;
}
case "KubrowPetEggs": {
for (const egg of value) {
inventory.KubrowPetEggs ??= [];
inventory.KubrowPetEggs.push({
ItemType: egg.ItemType,
_id: new Types.ObjectId()
});
}
break;
}
case "DiscoveredMarkers": {
for (const clientMarker of value) {
const dbMarker = inventory.DiscoveredMarkers.find(x => x.tag == clientMarker.tag);
@ -561,6 +599,76 @@ export const addMissionInventoryUpdates = async (
}
break;
}
case "CalendarProgress": {
const calendarProgress = getCalendarProgress(inventory);
for (const progress of value) {
const challengeName = progress.challenge.substring(progress.challenge.lastIndexOf("/") + 1);
calendarProgress.SeasonProgress.LastCompletedChallengeDayIdx++;
calendarProgress.SeasonProgress.ActivatedChallenges.push(challengeName);
}
break;
}
case "duviriCaveOffers": {
// Duviri cave offers (generated with the duviri seed) change after completing one of its game modes (not when aborting).
if (inventoryUpdates.MissionStatus != "GS_QUIT") {
inventory.DuviriInfo.Seed = generateRewardSeed();
}
break;
}
case "NemesisKillConvert":
if (inventory.Nemesis) {
inventory.NemesisHistory ??= [];
inventory.NemesisHistory.push({
// Copy over all 'base' values
fp: inventory.Nemesis.fp,
d: inventory.Nemesis.d,
manifest: inventory.Nemesis.manifest,
KillingSuit: inventory.Nemesis.KillingSuit,
killingDamageType: inventory.Nemesis.killingDamageType,
ShoulderHelmet: inventory.Nemesis.ShoulderHelmet,
WeaponIdx: inventory.Nemesis.WeaponIdx,
AgentIdx: inventory.Nemesis.AgentIdx,
BirthNode: inventory.Nemesis.BirthNode,
Faction: inventory.Nemesis.Faction,
Rank: inventory.Nemesis.Rank,
Traded: inventory.Nemesis.Traded,
PrevOwners: inventory.Nemesis.PrevOwners,
SecondInCommand: inventory.Nemesis.SecondInCommand,
Weakened: inventory.Nemesis.Weakened,
// And set killed flag
k: value.killed
});
if (value.killed) {
if (
value.weaponLoc &&
inventory.Nemesis.Faction != "FC_INFESTATION" // weaponLoc is "/Lotus/Language/Weapons/DerelictCernosName" for these for some reason
) {
const weaponType = getWeaponsForManifest(inventory.Nemesis.manifest)[
inventory.Nemesis.WeaponIdx
];
giveNemesisWeaponRecipe(
inventory,
weaponType,
value.nemesisName,
value.weaponLoc,
inventory.Nemesis.KillingSuit,
inventory.Nemesis.fp
);
}
if (value.petLoc) {
giveNemesisPetRecipe(inventory);
}
}
// TOVERIFY: Is the inbox message also sent when converting a lich? If not, how are the rewards given?
if (inventory.Nemesis.Faction == "FC_INFESTATION") {
await sendCodaFinishedMessage(inventory, inventory.Nemesis.fp, value.nemesisName, value.killed);
}
inventory.Nemesis = undefined;
}
break;
default:
// Equipment XP updates
if (equipmentKeys.includes(key as TEquipmentKey)) {
@ -586,8 +694,140 @@ interface AddMissionRewardsReturnType {
credits?: IMissionCredits;
AffiliationMods?: IAffiliationMods[];
SyndicateXPItemReward?: number;
ConquestCompletedMissionsCount?: number;
}
interface IConquestReward {
at: number;
pool: IRngResult[];
}
const labConquestRewards: IConquestReward[] = [
{
at: 5,
pool: ExportRewards[
"/Lotus/Types/Game/MissionDecks/EntratiLabConquestRewards/EntratiLabConquestSilverRewards"
][0] as IRngResult[]
},
{
at: 10,
pool: ExportRewards[
"/Lotus/Types/Game/MissionDecks/EntratiLabConquestRewards/EntratiLabConquestSilverRewards"
][0] as IRngResult[]
},
{
at: 15,
pool: [
{
type: "/Lotus/StoreItems/Types/Gameplay/EntratiLab/Resources/EntratiLanthornBundle",
itemCount: 3,
probability: 1
}
]
},
{
at: 20,
pool: ExportRewards[
"/Lotus/Types/Game/MissionDecks/EntratiLabConquestRewards/EntratiLabConquestGoldRewards"
][0] as IRngResult[]
},
{
at: 28,
pool: [
{
type: "/Lotus/StoreItems/Types/Items/MiscItems/DistillPoints",
itemCount: 20,
probability: 1
}
]
},
{
at: 31,
pool: ExportRewards[
"/Lotus/Types/Game/MissionDecks/EntratiLabConquestRewards/EntratiLabConquestGoldRewards"
][0] as IRngResult[]
},
{
at: 34,
pool: ExportRewards[
"/Lotus/Types/Game/MissionDecks/EntratiLabConquestRewards/EntratiLabConquestArcaneRewards"
][0] as IRngResult[]
},
{
at: 37,
pool: [
{
type: "/Lotus/StoreItems/Types/Items/MiscItems/DistillPoints",
itemCount: 50,
probability: 1
}
]
}
];
const hexConquestRewards: IConquestReward[] = [
{
at: 5,
pool: ExportRewards[
"/Lotus/Types/Game/MissionDecks/1999ConquestRewards/1999ConquestSilverRewards"
][0] as IRngResult[]
},
{
at: 10,
pool: ExportRewards[
"/Lotus/Types/Game/MissionDecks/1999ConquestRewards/1999ConquestSilverRewards"
][0] as IRngResult[]
},
{
at: 15,
pool: [
{
type: "/Lotus/StoreItems/Types/BoosterPacks/1999StickersPackEchoesArchimedea",
itemCount: 1,
probability: 1
}
]
},
{
at: 20,
pool: ExportRewards[
"/Lotus/Types/Game/MissionDecks/1999ConquestRewards/1999ConquestGoldRewards"
][0] as IRngResult[]
},
{
at: 28,
pool: [
{
type: "/Lotus/StoreItems/Types/Items/MiscItems/1999ConquestBucks",
itemCount: 6,
probability: 1
}
]
},
{
at: 31,
pool: ExportRewards[
"/Lotus/Types/Game/MissionDecks/1999ConquestRewards/1999ConquestGoldRewards"
][0] as IRngResult[]
},
{
at: 34,
pool: ExportRewards[
"/Lotus/Types/Game/MissionDecks/1999ConquestRewards/1999ConquestArcaneRewards"
][0] as IRngResult[]
},
{
at: 37,
pool: [
{
type: "/Lotus/StoreItems/Types/Items/MiscItems/1999ConquestBucks",
itemCount: 9,
probability: 1
}
]
}
];
//TODO: return type of partial missioninventoryupdate response
export const addMissionRewards = async (
inventory: TInventoryDatabaseDocument,
@ -609,17 +849,13 @@ export const addMissionRewards = async (
return { MissionRewards: [] };
}
if (rewardInfo.rewardSeed) {
// We're using a reward seed, so give the client a new one in the response. On live, missionInventoryUpdate seems to always provide a fresh one in the response.
inventory.RewardSeed = generateRewardSeed();
}
//TODO: check double reward merging
const MissionRewards: IMissionReward[] = getRandomMissionDrops(inventory, rewardInfo, wagerTier, firstCompletion);
logger.debug("random mission drops:", MissionRewards);
const inventoryChanges: IInventoryChanges = {};
const AffiliationMods: IAffiliationMods[] = [];
let SyndicateXPItemReward;
let ConquestCompletedMissionsCount;
let missionCompletionCredits = 0;
//inventory change is what the client has not rewarded itself, also the client needs to know the credit changes for display
@ -658,7 +894,8 @@ export const addMissionRewards = async (
missions.Tag != "SolNode761" && // the index
missions.Tag != "SolNode762" && // the index
missions.Tag != "SolNode763" && // the index
missions.Tag != "CrewBattleNode556" // free flight
missions.Tag != "CrewBattleNode556" && // free flight
getRotations(rewardInfo).length > 0 // (E)SO should not give credits for only completing zone 1, in which case it has no rewardQualifications (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1823)
) {
const levelCreditReward = getLevelCreditRewards(node);
missionCompletionCredits += levelCreditReward;
@ -670,6 +907,14 @@ export const addMissionRewards = async (
missionCompletionCredits += addFixedLevelRewards(node.missionReward, inventory, MissionRewards, rewardInfo);
}
if (rewardInfo.sortieTag == "Mission1") {
missionCompletionCredits += 20_000;
} else if (rewardInfo.sortieTag == "Mission2") {
missionCompletionCredits += 30_000;
} else if (rewardInfo.sortieTag == "Final") {
missionCompletionCredits += 50_000;
}
if (missions.Tag == "PlutoToErisJunction") {
await createMessage(inventory.accountOwnerId, [
{
@ -686,11 +931,67 @@ export const addMissionRewards = async (
if (rewardInfo.useVaultManifest) {
MissionRewards.push({
StoreItem: getRandomElement(corruptedMods),
StoreItem: getRandomElement(corruptedMods)!,
ItemCount: 1
});
}
if (rewardInfo.ConquestCompleted !== undefined) {
let score = 1;
if (rewardInfo.ConquestHardModeActive === 1) score += 3;
if (rewardInfo.ConquestPersonalModifiersActive !== undefined)
score += rewardInfo.ConquestPersonalModifiersActive;
if (rewardInfo.ConquestEquipmentSuggestionsFulfilled !== undefined)
score += rewardInfo.ConquestEquipmentSuggestionsFulfilled;
score *= rewardInfo.ConquestCompleted + 1;
if (rewardInfo.ConquestCompleted == 2 && rewardInfo.ConquestHardModeActive === 1) score += 1;
logger.debug(`completed conquest mission ${rewardInfo.ConquestCompleted + 1} for a score of ${score}`);
const conquestType = rewardInfo.ConquestType;
const conquestNode =
conquestType == "HexConquest" ? "EchoesHexConquestHardModeUnlocked" : "EntratiLabConquestHardModeUnlocked";
if (score >= 25 && inventory.NodeIntrosCompleted.indexOf(conquestNode) == -1)
inventory.NodeIntrosCompleted.push(conquestNode);
if (conquestType == "HexConquest") {
inventory.EchoesHexConquestCacheScoreMission ??= 0;
if (score > inventory.EchoesHexConquestCacheScoreMission) {
for (const reward of hexConquestRewards) {
if (score >= reward.at && inventory.EchoesHexConquestCacheScoreMission < reward.at) {
const rolled = getRandomReward(reward.pool)!;
logger.debug(`rolled hex conquest reward for reaching ${reward.at} points`, rolled);
MissionRewards.push({
StoreItem: rolled.type,
ItemCount: rolled.itemCount
});
}
}
inventory.EchoesHexConquestCacheScoreMission = score;
}
} else {
inventory.EntratiLabConquestCacheScoreMission ??= 0;
if (score > inventory.EntratiLabConquestCacheScoreMission) {
for (const reward of labConquestRewards) {
if (score >= reward.at && inventory.EntratiLabConquestCacheScoreMission < reward.at) {
const rolled = getRandomReward(reward.pool)!;
logger.debug(`rolled lab conquest reward for reaching ${reward.at} points`, rolled);
MissionRewards.push({
StoreItem: rolled.type,
ItemCount: rolled.itemCount
});
}
}
inventory.EntratiLabConquestCacheScoreMission = score;
}
}
ConquestCompletedMissionsCount = rewardInfo.ConquestCompleted == 2 ? 0 : rewardInfo.ConquestCompleted + 1;
}
for (const reward of MissionRewards) {
const inventoryChange = await handleStoreItemAcquisition(
reward.StoreItem,
@ -882,15 +1183,15 @@ export const addMissionRewards = async (
}
}
return { inventoryChanges, MissionRewards, credits, AffiliationMods, SyndicateXPItemReward };
return {
inventoryChanges,
MissionRewards,
credits,
AffiliationMods,
SyndicateXPItemReward,
ConquestCompletedMissionsCount
};
};
interface IMissionCredits {
MissionCredits: number[];
CreditBonus: number[];
TotalCredits: number[];
DailyMissionBonus?: boolean;
}
//creditBonus is not entirely accurate.
//TODO: consider ActiveBoosters
@ -1068,6 +1369,9 @@ function getRandomMissionDrops(
// Invasion assassination has Phorid has the boss who should drop Nyx parts
// TODO: Check that the invasion faction is indeed FC_INFESTATION once the Invasions in worldState are more dynamic
rewardManifests = ["/Lotus/Types/Game/MissionDecks/BossMissionRewards/NyxRewards"];
} else if (RewardInfo.sortieId && region.missionIndex != 0) {
// Sortie mission types differ from the underlying node and hence also don't give rewards from the underlying nodes. Assassinations are an exception to this.
rewardManifests = [];
} else {
rewardManifests = region.rewardManifests;
}
@ -1216,6 +1520,11 @@ function getRandomMissionDrops(
if (rewardManifests.length != 0) {
logger.debug(`generating random mission rewards`, { rewardManifests, rotations });
}
if (RewardInfo.rewardSeed) {
if (RewardInfo.rewardSeed != inventory.RewardSeed) {
logger.warn(`RewardSeed mismatch:`, { client: RewardInfo.rewardSeed, database: inventory.RewardSeed });
}
}
const rng = new SRng(BigInt(RewardInfo.rewardSeed ?? generateRewardSeed()) ^ 0xffffffffffffffffn);
rewardManifests.forEach(name => {
const table = ExportRewards[name];

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,
@ -170,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",
@ -223,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,
@ -239,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) {

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);
@ -97,12 +100,20 @@ 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)];
}
@ -134,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)];
}

View File

@ -161,6 +161,11 @@ export const handleInventoryItemConfigChange = async (
}
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}`, {

View File

@ -1,17 +1,12 @@
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,
IVendorInfo,
IVendorManifestPreprocessed
} from "@/src/types/vendorTypes";
import { ExportVendors } from "warframe-public-export-plus";
import { IItemManifest, IVendorInfo, IVendorManifest } from "@/src/types/vendorTypes";
import { ExportVendors, IRange } from "warframe-public-export-plus";
import ArchimedeanVendorManifest from "@/static/fixed_responses/getVendorInfo/ArchimedeanVendorManifest.json";
import DeimosEntratiFragmentVendorProductsManifest from "@/static/fixed_responses/getVendorInfo/DeimosEntratiFragmentVendorProductsManifest.json";
import DeimosFishmongerVendorManifest from "@/static/fixed_responses/getVendorInfo/DeimosFishmongerVendorManifest.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";
@ -23,26 +18,22 @@ import DeimosProspectorVendorManifest from "@/static/fixed_responses/getVendorIn
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 GuildAdvertisementVendorManifest from "@/static/fixed_responses/getVendorInfo/GuildAdvertisementVendorManifest.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 OstronFishmongerVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronFishmongerVendorManifest.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 SolarisDebtTokenVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorManifest.json";
import SolarisDebtTokenVendorRepossessionsManifest from "@/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorRepossessionsManifest.json";
import SolarisFishmongerVendorManifest from "@/static/fixed_responses/getVendorInfo/SolarisFishmongerVendorManifest.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[] = [
const rawVendorManifests: IVendorManifest[] = [
ArchimedeanVendorManifest,
DeimosEntratiFragmentVendorProductsManifest,
DeimosFishmongerVendorManifest,
DeimosHivemindCommisionsManifestFishmonger,
DeimosHivemindCommisionsManifestPetVendor,
DeimosHivemindCommisionsManifestProspector,
@ -54,25 +45,22 @@ const rawVendorManifests: IRawVendorManifest[] = [
DuviriAcrithisVendorManifest,
EntratiLabsEntratiLabsCommisionsManifest,
EntratiLabsEntratiLabVendorManifest,
GuildAdvertisementVendorManifest, // uses preprocessing
HubsIronwakeDondaVendorManifest, // uses preprocessing
HubsRailjackCrewMemberVendorManifest,
MaskSalesmanManifest,
Nova1999ConquestShopManifest,
OstronFishmongerVendorManifest,
OstronPetVendorManifest,
OstronProspectorVendorManifest,
RadioLegionIntermission12VendorManifest,
SolarisDebtTokenVendorManifest,
SolarisDebtTokenVendorRepossessionsManifest,
SolarisFishmongerVendorManifest,
SolarisProspectorVendorManifest,
Temple1999VendorManifest,
TeshinHardModeVendorManifest, // uses preprocessing
ZarimanCommisionsManifestArchimedean
];
interface IGeneratableVendorInfo extends Omit<IVendorInfo, "ItemManifest" | "Expiry"> {
cycleStart: number;
cycleOffset?: number;
cycleDuration: number;
}
@ -80,30 +68,37 @@ const generatableVendors: IGeneratableVendorInfo[] = [
{
_id: { $oid: "67dadc30e4b6e0e5979c8d84" },
TypeName: "/Lotus/Types/Game/VendorManifests/TheHex/InfestedLichWeaponVendorManifest",
PropertyTextHash: "77093DD05A8561A022DEC9A4B9BB4A56",
RandomSeedType: "VRST_WEAPON",
RequiredGoalTag: "",
WeaponUpgradeValueAttenuationExponent: 2.25,
cycleStart: 1740960000_000,
cycleOffset: 1740960000_000,
cycleDuration: 4 * unixTimesInMs.day
},
{
_id: { $oid: "60ad3b6ec96976e97d227e19" },
TypeName: "/Lotus/Types/Game/VendorManifests/Hubs/PerrinSequenceWeaponVendorManifest",
PropertyTextHash: "34F8CF1DFF745F0D67433A5EF0A03E70",
RandomSeedType: "VRST_WEAPON",
WeaponUpgradeValueAttenuationExponent: 2.25,
cycleStart: 1744934400_000,
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",
// PropertyTextHash: "62B64A8065B7C0FA345895D4BC234621"
// TypeName: "/Lotus/Types/Game/VendorManifests/Hubs/IronwakeDondaVendorManifest"
// }
];
export const getVendorManifestByTypeName = (typeName: string): IVendorManifestPreprocessed | undefined => {
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);
@ -114,10 +109,18 @@ export const getVendorManifestByTypeName = (typeName: string): IVendorManifestPr
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);
@ -128,32 +131,34 @@ export const getVendorManifestByOid = (oid: string): IVendorManifestPreprocessed
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;
}
}
}
return manifest as IVendorManifestPreprocessed;
}
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_000; // Monday (for weekly schedules)
@ -161,65 +166,143 @@ const refreshExpiry = (expiry: IMongoDate): number => {
const start = epoch + iteration * period;
const end = start + period;
expiry.$date.$numberLong = end.toString();
return iteration;
}
return 0;
};
const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorManifestPreprocessed => {
const EPOCH = vendorInfo.cycleStart;
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;
}
}
// 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) {
const cycleDuration = vendorInfo.cycleDuration; // manifest.items[0].durationHours! * 3600_000;
const cycleIndex = Math.trunc((Date.now() - EPOCH) / cycleDuration);
binThisCycle = cycleIndex % 2; // Note: May want to auto-compute the bin size, but this is only used for coda weapons right now.
}
const items: IItemManifestPreprocessed[] = [];
let soonestOfferExpiry: number = Number.MAX_SAFE_INTEGER;
for (let i = 0; i != manifest.items.length; ++i) {
const rawItem = manifest.items[i];
if (manifest.isOneBinPerCycle && rawItem.bin != binThisCycle) {
continue;
for (const rawItem of manifest.items) {
if (!manifest.isOneBinPerCycle || rawItem.bin == binThisCycle) {
offersToAdd.push(rawItem);
}
const cycleDuration = vendorInfo.cycleDuration; // rawItem.durationHours! * 3600_000;
const cycleIndex = Math.trunc((Date.now() - EPOCH) / cycleDuration);
const cycleStart = EPOCH + cycleIndex * cycleDuration;
const cycleEnd = cycleStart + cycleDuration;
if (soonestOfferExpiry > cycleEnd) {
soonestOfferExpiry = cycleEnd;
}
const rng = new CRng(cycleIndex);
rng.churnSeed(i);
/*for (let j = -1; j != rawItem.duplicates; ++j)*/ {
const item: IItemManifestPreprocessed = {
// 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" })),
ItemPrices: rawItem.itemPrices?.map(itemPrice => ({ ...itemPrice, ProductCategory: "MiscItems" })),
Bin: "BIN_" + rawItem.bin,
QuantityMultiplier: 1,
Expiry: { $date: { $numberLong: cycleEnd.toString() } },
Expiry: { $date: { $numberLong: expiry.toString() } },
AllowMultipurchase: false,
Id: {
$oid:
i.toString(16).padStart(8, "0") +
((cycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") +
vendorInfo._id.$oid.substring(8, 16) +
rng.randomInt(0, 0xffffffff).toString(16).padStart(8, "0")
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 =
(BigInt(rng.randomInt(0, 0xffffffff)) << 32n) | BigInt(rng.randomInt(0, 0xffffffff));
}
items.push(item);
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);
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { cycleStart, cycleDuration, ...clientVendorInfo } = vendorInfo;
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: {
...clientVendorInfo,
ItemManifest: items,
Expiry: { $date: { $numberLong: soonestOfferExpiry.toString() } }
}
VendorInfo: processed
};
};

View File

@ -1,10 +1,20 @@
import staticWorldState from "@/static/fixed_responses/worldState/worldState.json";
import sortieTilesets from "@/static/fixed_responses/worldState/sortieTilesets.json";
import syndicateMissions from "@/static/fixed_responses/worldState/syndicateMissions.json";
import { buildConfig } from "@/src/services/buildConfigService";
import { unixTimesInMs } from "@/src/constants/timeConstants";
import { config } from "@/src/services/configService";
import { CRng } from "@/src/services/rngService";
import { eMissionType, ExportNightwave, ExportRegions } from "warframe-public-export-plus";
import { ICalendarDay, ICalendarSeason, ILiteSortie, ISeasonChallenge, IWorldState } from "../types/worldStateTypes";
import { eMissionType, ExportNightwave, ExportRegions, IRegion } from "warframe-public-export-plus";
import {
ICalendarDay,
ICalendarEvent,
ICalendarSeason,
ILiteSortie,
ISeasonChallenge,
ISortieMission,
IWorldState
} from "../types/worldStateTypes";
const sortieBosses = [
"SORTIE_BOSS_HYENA",
@ -41,21 +51,27 @@ const sortieBossToFaction: Record<string, string> = {
SORTIE_BOSS_PHORID: "FC_INFESTATION",
SORTIE_BOSS_LEPHANTIS: "FC_INFESTATION",
SORTIE_BOSS_INFALAD: "FC_INFESTATION",
SORTIE_BOSS_CORRUPTED_VOR: "FC_CORRUPTED"
SORTIE_BOSS_CORRUPTED_VOR: "FC_OROKIN"
};
const sortieFactionToSystemIndexes: Record<string, number[]> = {
FC_GRINEER: [0, 2, 3, 5, 6, 9, 11, 18],
FC_CORPUS: [1, 4, 7, 8, 12, 15],
FC_INFESTATION: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 15],
FC_CORRUPTED: [14]
FC_OROKIN: [14]
};
const sortieFactionToFactionIndexes: Record<string, number[]> = {
FC_GRINEER: [0],
FC_CORPUS: [1],
FC_INFESTATION: [0, 1, 2],
FC_CORRUPTED: [3]
FC_OROKIN: [3]
};
const sortieFactionToSpecialMissionTileset: Record<string, string> = {
FC_GRINEER: "GrineerGalleonTileset",
FC_CORPUS: "CorpusShipTileset",
FC_INFESTATION: "CorpusShipTileset"
};
const sortieBossNode: Record<string, string> = {
@ -174,18 +190,7 @@ const pushSyndicateMissions = (
idSuffix: string,
syndicateTag: string
): void => {
const nodeOptions: string[] = [];
for (const [key, value] of Object.entries(ExportRegions)) {
if (
value.name.indexOf("Archwing") == -1 && // no archwing
value.systemIndex != 23 && // no 1999 stuff
value.missionIndex != 10 && // Exclude MT_PVP (for relays)
value.missionIndex != 23 && // no junctions
value.missionIndex <= 28 // no railjack or some such
) {
nodeOptions.push(key);
}
}
const nodeOptions: string[] = [...syndicateMissions];
const rng = new CRng(seed);
const nodes: string[] = [];
@ -198,7 +203,7 @@ const pushSyndicateMissions = (
const dayStart = getSortieTime(day);
const dayEnd = getSortieTime(day + 1);
worldState.SyndicateMissions.push({
_id: { $oid: Math.trunc(dayStart / 1000).toString(16) + idSuffix },
_id: { $oid: ((dayStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + idSuffix },
Activation: { $date: { $numberLong: dayStart.toString() } },
Expiry: { $date: { $numberLong: dayEnd.toString() } },
Tag: syndicateTag,
@ -217,9 +222,10 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => {
return;
}
const rng = new CRng(day);
const seed = new CRng(day).randomInt(0, 0xffff);
const rng = new CRng(seed);
const boss = rng.randomElement(sortieBosses);
const boss = rng.randomElement(sortieBosses)!;
const modifiers = [
"SORTIE_MODIFIER_LOW_ENERGY",
@ -261,48 +267,69 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => {
if (
sortieFactionToSystemIndexes[sortieBossToFaction[boss]].includes(value.systemIndex) &&
sortieFactionToFactionIndexes[sortieBossToFaction[boss]].includes(value.factionIndex!) &&
value.name.indexOf("Archwing") == -1 &&
value.missionIndex != 0 && // Exclude MT_ASSASSINATION
value.missionIndex != 5 && // Exclude MT_CAPTURE
value.missionIndex != 10 && // Exclude MT_PVP (for relays)
value.missionIndex != 21 && // Exclude MT_PURIFY
value.missionIndex != 22 && // Exclude MT_ARENA
value.missionIndex != 23 && // Exclude MT_JUNCTION
value.missionIndex <= 28
key in sortieTilesets
) {
if (
value.missionIndex != 5 && // Sorties do not have capture missions
!availableMissionIndexes.includes(value.missionIndex)
) {
if (!availableMissionIndexes.includes(value.missionIndex)) {
availableMissionIndexes.push(value.missionIndex);
}
nodes.push(key);
}
}
const selectedNodes: { missionType: string; modifierType: string; node: string }[] = [];
const specialMissionTypes = [1, 3, 5, 9];
if (!(sortieBossToFaction[boss] in sortieFactionToSpecialMissionTileset)) {
for (const missionType of specialMissionTypes) {
const i = availableMissionIndexes.indexOf(missionType);
if (i != -1) {
availableMissionIndexes.splice(i, 1);
}
}
}
const selectedNodes: ISortieMission[] = [];
const missionTypes = new Set();
for (let i = 0; i < 3; i++) {
const randomIndex = rng.randomInt(0, nodes.length - 1);
const node = nodes[randomIndex];
let missionIndex = ExportRegions[node].missionIndex;
let randomIndex;
let node;
let missionIndex;
do {
randomIndex = rng.randomInt(0, nodes.length - 1);
node = nodes[randomIndex];
if (
!["SolNode404", "SolNode411"].includes(node) && // for some reason the game doesn't like missionType changes for these missions
missionIndex != 28 &&
rng.randomInt(0, 2) == 2
) {
missionIndex = rng.randomElement(availableMissionIndexes);
missionIndex = ExportRegions[node].missionIndex;
if (missionIndex != 28) {
missionIndex = rng.randomElement(availableMissionIndexes)!;
}
} while (
specialMissionTypes.indexOf(missionIndex) != -1 &&
sortieTilesets[node as keyof typeof sortieTilesets] !=
sortieFactionToSpecialMissionTileset[sortieBossToFaction[boss]]
);
if (i == 2 && rng.randomInt(0, 2) == 2) {
const filteredModifiers = modifiers.filter(mod => mod !== "SORTIE_MODIFIER_MELEE_ONLY");
const modifierType = rng.randomElement(filteredModifiers);
const modifierType = rng.randomElement(filteredModifiers)!;
if (boss == "SORTIE_BOSS_PHORID") {
selectedNodes.push({ missionType: "MT_ASSASSINATION", modifierType, node });
selectedNodes.push({
missionType: "MT_ASSASSINATION",
modifierType,
node,
tileset: sortieTilesets[node as keyof typeof sortieTilesets]
});
nodes.splice(randomIndex, 1);
continue;
} else if (sortieBossNode[boss]) {
selectedNodes.push({ missionType: "MT_ASSASSINATION", modifierType, node: sortieBossNode[boss] });
selectedNodes.push({
missionType: "MT_ASSASSINATION",
modifierType,
node: sortieBossNode[boss],
tileset: sortieTilesets[sortieBossNode[boss] as keyof typeof sortieTilesets]
});
continue;
}
}
@ -319,19 +346,24 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => {
? modifiers.filter(mod => mod != "SORTIE_MODIFIER_HAZARD_RADIATION")
: modifiers;
const modifierType = rng.randomElement(filteredModifiers);
const modifierType = rng.randomElement(filteredModifiers)!;
selectedNodes.push({ missionType, modifierType, node });
selectedNodes.push({
missionType,
modifierType,
node,
tileset: sortieTilesets[node as keyof typeof sortieTilesets]
});
nodes.splice(randomIndex, 1);
missionTypes.add(missionType);
}
worldState.Sorties.push({
_id: { $oid: Math.trunc(dayStart / 1000).toString(16) + "d4d932c97c0a3acd" },
_id: { $oid: ((dayStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "d4d932c97c0a3acd" },
Activation: { $date: { $numberLong: dayStart.toString() } },
Expiry: { $date: { $numberLong: dayEnd.toString() } },
Reward: "/Lotus/Types/Game/MissionDecks/SortieRewards",
Seed: day,
Seed: seed,
Boss: boss,
Variants: selectedNodes
});
@ -351,13 +383,13 @@ const dailyChallenges = Object.keys(ExportNightwave.challenges).filter(x =>
const getSeasonDailyChallenge = (day: number): ISeasonChallenge => {
const dayStart = EPOCH + day * 86400000;
const dayEnd = EPOCH + (day + 3) * 86400000;
const rng = new CRng(day);
const rng = new CRng(new CRng(day).randomInt(0, 0xffff));
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)
Challenge: rng.randomElement(dailyChallenges)!
};
};
@ -371,12 +403,12 @@ 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);
const rng = new CRng(new CRng(challengeId).randomInt(0, 0xffff));
return {
_id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") },
Activation: { $date: { $numberLong: weekStart.toString() } },
Expiry: { $date: { $numberLong: weekEnd.toString() } },
Challenge: rng.randomElement(weeklyChallenges)
Challenge: rng.randomElement(weeklyChallenges)!
};
};
@ -388,12 +420,12 @@ const getSeasonWeeklyHardChallenge = (week: number, id: number): ISeasonChalleng
const weekStart = EPOCH + week * 604800000;
const weekEnd = weekStart + 604800000;
const challengeId = week * 7 + id;
const rng = new CRng(challengeId);
const rng = new CRng(new CRng(challengeId).randomInt(0, 0xffff));
return {
_id: { $oid: "67e1bb2d9d00cb47" + challengeId.toString().padStart(8, "0") },
Activation: { $date: { $numberLong: weekStart.toString() } },
Expiry: { $date: { $numberLong: weekEnd.toString() } },
Challenge: rng.randomElement(weeklyHardChallenges)
Challenge: rng.randomElement(weeklyHardChallenges)!
};
};
@ -440,8 +472,8 @@ const birthdays: number[] = [
const getCalendarSeason = (week: number): ICalendarSeason => {
const seasonIndex = week % 4;
const seasonDay1 = seasonIndex * 90 + 1;
const seasonDay91 = seasonIndex * 90 + 91;
const seasonDay1 = [1, 91, 182, 274][seasonIndex];
const seasonDay91 = seasonDay1 + 90;
const eventDays: ICalendarDay[] = [];
for (const day of birthdays) {
if (day < seasonDay1) {
@ -453,7 +485,7 @@ const getCalendarSeason = (week: number): ICalendarSeason => {
//logger.debug(`birthday on day ${day}`);
eventDays.push({ day, events: [] }); // This is how CET_PLOT looks in worldState as of around 38.5.0
}
const rng = new CRng(week);
const rng = new CRng(new CRng(week).randomInt(0, 0xffff));
const challenges = [
"/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesEasy",
"/Lotus/Types/Challenges/Calendar1999/CalendarKillEnemiesMedium",
@ -500,8 +532,12 @@ const getCalendarSeason = (week: number): ICalendarSeason => {
challengeDay = rng.randomInt(chunkDay1, chunkDay13);
} while (birthdays.indexOf(challengeDay) != -1);
const challengeIndex = rng.randomInt(0, challenges.length - 1);
const challenge = challenges[challengeIndex];
let challengeIndex;
let challenge;
do {
challengeIndex = rng.randomInt(0, challenges.length - 1);
challenge = challenges[challengeIndex];
} while (i < 2 && !challenge.endsWith("Easy")); // First 2 challenges should be easy
challenges.splice(challengeIndex, 1);
//logger.debug(`challenge on day ${challengeDay}`);
@ -548,69 +584,100 @@ const getCalendarSeason = (week: number): ICalendarSeason => {
"/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalViolet"
];
for (let i = 0; i != rewardRanges.length - 1; ++i) {
const events: ICalendarEvent[] = [];
for (let j = 0; j != 2; ++j) {
const rewardIndex = rng.randomInt(0, rewards.length - 1);
const reward = rewards[rewardIndex];
events.push({ type: "CET_REWARD", reward: rewards[rewardIndex] });
rewards.splice(rewardIndex, 1);
}
//logger.debug(`trying to fit a reward between day ${rewardRanges[i]} and ${rewardRanges[i + 1]}`);
//logger.debug(`trying to fit rewards between day ${rewardRanges[i]} and ${rewardRanges[i + 1]}`);
let day: number;
do {
day = rng.randomInt(rewardRanges[i] + 1, rewardRanges[i + 1] - 1);
} while (eventDays.find(x => x.day == day));
eventDays.push({ day, events: [{ type: "CET_REWARD", reward }] });
eventDays.push({ day, events });
}
const upgrades = [
"/Lotus/Upgrades/Calendar/MeleeCritChance",
"/Lotus/Upgrades/Calendar/MeleeAttackSpeed",
"/Lotus/Upgrades/Calendar/EnergyOrbToAbilityRange",
const upgradesByHexMember = [
[
"/Lotus/Upgrades/Calendar/AttackAndMovementSpeedOnCritMelee",
"/Lotus/Upgrades/Calendar/ElectricalDamageOnBulletJump",
"/Lotus/Upgrades/Calendar/ElectricDamagePerDistance",
"/Lotus/Upgrades/Calendar/ElectricStatusDamageAndChance",
"/Lotus/Upgrades/Calendar/OvershieldCap",
"/Lotus/Upgrades/Calendar/SpeedBuffsWhenAirborne"
],
[
"/Lotus/Upgrades/Calendar/AbilityStrength",
"/Lotus/Upgrades/Calendar/EnergyOrbToAbilityRange",
"/Lotus/Upgrades/Calendar/MagnetStatusPull",
"/Lotus/Upgrades/Calendar/MagnitizeWithinRangeEveryXCasts",
"/Lotus/Upgrades/Calendar/PowerStrengthAndEfficiencyPerEnergySpent",
"/Lotus/Upgrades/Calendar/SharedFreeAbilityEveryXCasts"
],
[
"/Lotus/Upgrades/Calendar/EnergyWavesOnCombo",
"/Lotus/Upgrades/Calendar/FinisherChancePerComboMultiplier",
"/Lotus/Upgrades/Calendar/MeleeAttackSpeed",
"/Lotus/Upgrades/Calendar/MeleeCritChance",
"/Lotus/Upgrades/Calendar/MeleeSlideFowardMomentumOnEnemyHit",
"/Lotus/Upgrades/Calendar/RadialJavelinOnHeavy"
],
[
"/Lotus/Upgrades/Calendar/Armor",
"/Lotus/Upgrades/Calendar/RadiationProcOnTakeDamage",
"/Lotus/Upgrades/Calendar/CloneActiveCompanionForEnergySpent",
"/Lotus/Upgrades/Calendar/CompanionDamage",
"/Lotus/Upgrades/Calendar/CompanionsBuffNearbyPlayer",
"/Lotus/Upgrades/Calendar/CompanionsRadiationChance",
"/Lotus/Upgrades/Calendar/RadiationProcOnTakeDamage",
"/Lotus/Upgrades/Calendar/ReviveEnemyAsSpectreOnKill"
],
[
"/Lotus/Upgrades/Calendar/EnergyOrbsGrantShield",
"/Lotus/Upgrades/Calendar/EnergyRestoration",
"/Lotus/Upgrades/Calendar/ExplodingHealthOrbs",
"/Lotus/Upgrades/Calendar/GenerateOmniOrbsOnWeakKill",
"/Lotus/Upgrades/Calendar/HealingEffects",
"/Lotus/Upgrades/Calendar/OrbsDuplicateOnPickup"
],
[
"/Lotus/Upgrades/Calendar/BlastEveryXShots",
"/Lotus/Upgrades/Calendar/GasChanceToPrimaryAndSecondary",
"/Lotus/Upgrades/Calendar/GuidingMissilesChance",
"/Lotus/Upgrades/Calendar/MagazineCapacity",
"/Lotus/Upgrades/Calendar/PunchToPrimary",
"/Lotus/Upgrades/Calendar/HealingEffects",
"/Lotus/Upgrades/Calendar/EnergyRestoration",
"/Lotus/Upgrades/Calendar/OvershieldCap",
"/Lotus/Upgrades/Calendar/ElectricStatusDamageAndChance",
"/Lotus/Upgrades/Calendar/FinisherChancePerComboMultiplier",
"/Lotus/Upgrades/Calendar/MagnetStatusPull",
"/Lotus/Upgrades/Calendar/CompanionsBuffNearbyPlayer",
"/Lotus/Upgrades/Calendar/StatusChancePerAmmoSpent",
"/Lotus/Upgrades/Calendar/OrbsDuplicateOnPickup",
"/Lotus/Upgrades/Calendar/AttackAndMovementSpeedOnCritMelee",
"/Lotus/Upgrades/Calendar/RadialJavelinOnHeavy",
"/Lotus/Upgrades/Calendar/MagnitizeWithinRangeEveryXCasts",
"/Lotus/Upgrades/Calendar/CompanionsRadiationChance",
"/Lotus/Upgrades/Calendar/BlastEveryXShots",
"/Lotus/Upgrades/Calendar/GenerateOmniOrbsOnWeakKill",
"/Lotus/Upgrades/Calendar/ElectricDamagePerDistance",
"/Lotus/Upgrades/Calendar/MeleeSlideFowardMomentumOnEnemyHit",
"/Lotus/Upgrades/Calendar/SharedFreeAbilityEveryXCasts",
"/Lotus/Upgrades/Calendar/ReviveEnemyAsSpectreOnKill",
"/Lotus/Upgrades/Calendar/RefundBulletOnStatusProc",
"/Lotus/Upgrades/Calendar/ExplodingHealthOrbs",
"/Lotus/Upgrades/Calendar/SpeedBuffsWhenAirborne",
"/Lotus/Upgrades/Calendar/EnergyWavesOnCombo",
"/Lotus/Upgrades/Calendar/PowerStrengthAndEfficiencyPerEnergySpent",
"/Lotus/Upgrades/Calendar/CloneActiveCompanionForEnergySpent",
"/Lotus/Upgrades/Calendar/GuidingMissilesChance",
"/Lotus/Upgrades/Calendar/EnergyOrbsGrantShield",
"/Lotus/Upgrades/Calendar/ElectricalDamageOnBulletJump"
"/Lotus/Upgrades/Calendar/StatusChancePerAmmoSpent"
]
];
for (let i = 0; i != upgradeRanges.length - 1; ++i) {
const upgradeIndex = rng.randomInt(0, upgrades.length - 1);
const upgrade = upgrades[upgradeIndex];
upgrades.splice(upgradeIndex, 1);
// Pick 3 unique hex members
const hexMembersPickedForThisDay: number[] = [];
for (let j = 0; j != 3; ++j) {
let hexMemberIndex: number;
do {
hexMemberIndex = rng.randomInt(0, upgradesByHexMember.length - 1);
} while (hexMembersPickedForThisDay.indexOf(hexMemberIndex) != -1);
hexMembersPickedForThisDay.push(hexMemberIndex);
}
hexMembersPickedForThisDay.sort(); // Always present them in the same order
//logger.debug(`trying to fit an upgrade between day ${upgradeRanges[i]} and ${upgradeRanges[i + 1]}`);
// For each hex member, pick an upgrade that was not yet picked this season.
const events: ICalendarEvent[] = [];
for (const hexMemberIndex of hexMembersPickedForThisDay) {
const upgrades = upgradesByHexMember[hexMemberIndex];
const upgradeIndex = rng.randomInt(0, upgrades.length - 1);
events.push({ type: "CET_UPGRADE", upgrade: upgrades[upgradeIndex] });
upgrades.splice(upgradeIndex, 1);
}
//logger.debug(`trying to fit upgrades between day ${upgradeRanges[i]} and ${upgradeRanges[i + 1]}`);
let day: number;
do {
day = rng.randomInt(upgradeRanges[i] + 1, upgradeRanges[i + 1] - 1);
} while (eventDays.find(x => x.day == day));
eventDays.push({ day, events: [{ type: "CET_UPGRADE", upgrade }] });
eventDays.push({ day, events });
}
eventDays.sort((a, b) => a.day - b.day);
@ -621,7 +688,7 @@ const getCalendarSeason = (week: number): ICalendarSeason => {
Activation: { $date: { $numberLong: weekStart.toString() } },
Expiry: { $date: { $numberLong: weekEnd.toString() } },
Days: eventDays,
Season: ["CST_WINTER", "CST_SPRING", "CST_SUMMER", "CST_FALL"][seasonIndex],
Season: (["CST_WINTER", "CST_SPRING", "CST_SUMMER", "CST_FALL"] as const)[seasonIndex],
YearIteration: Math.trunc(week / 4),
Version: 19,
UpgradeAvaliabilityRequirements: ["/Lotus/Upgrades/Calendar/1999UpgradeApplicationRequirement"]
@ -687,7 +754,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
}
// Elite Sanctuary Onslaught cycling every week
worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = week; // unfaithful
worldState.NodeOverrides.find(x => x.Node == "SolNode802")!.Seed = new CRng(week).randomInt(0, 0xffff);
// Holdfast, Cavia, & Hex bounties cycling every 2.5 hours; unfaithful implementation
let bountyCycle = Math.trunc(Date.now() / 9000000);
@ -696,7 +763,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
const bountyCycleStart = bountyCycle * 9000000;
bountyCycleEnd = bountyCycleStart + 9000000;
worldState.SyndicateMissions.push({
_id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000029" },
_id: { $oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000029" },
Activation: { $date: { $numberLong: bountyCycleStart.toString() } },
Expiry: { $date: { $numberLong: bountyCycleEnd.toString() } },
Tag: "ZarimanSyndicate",
@ -704,7 +771,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
Nodes: []
});
worldState.SyndicateMissions.push({
_id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000004" },
_id: { $oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000004" },
Activation: { $date: { $numberLong: bountyCycleStart.toString() } },
Expiry: { $date: { $numberLong: bountyCycleEnd.toString() } },
Tag: "EntratiLabSyndicate",
@ -712,7 +779,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
Nodes: []
});
worldState.SyndicateMissions.push({
_id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000006" },
_id: { $oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000006" },
Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } },
Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } },
Tag: "HexSyndicate",
@ -726,14 +793,18 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
// TODO: xpAmounts need to be calculated based on the jobType somehow?
const seed = new CRng(bountyCycle).randomInt(0, 0xffff);
{
const rng = new CRng(bountyCycle);
const rng = new CRng(seed);
worldState.SyndicateMissions.push({
_id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000008" },
_id: {
$oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000008"
},
Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } },
Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } },
Tag: "CetusSyndicate",
Seed: bountyCycle,
Seed: seed,
Nodes: [],
Jobs: [
{
@ -797,13 +868,15 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
}
{
const rng = new CRng(bountyCycle);
const rng = new CRng(seed);
worldState.SyndicateMissions.push({
_id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000025" },
_id: {
$oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000025"
},
Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } },
Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } },
Tag: "SolarisSyndicate",
Seed: bountyCycle,
Seed: seed,
Nodes: [],
Jobs: [
{
@ -867,13 +940,15 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
}
{
const rng = new CRng(bountyCycle);
const rng = new CRng(seed);
worldState.SyndicateMissions.push({
_id: { $oid: Math.trunc(bountyCycleStart / 1000).toString(16) + "0000000000000002" },
_id: {
$oid: ((bountyCycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "0000000000000002"
},
Activation: { $date: { $numberLong: bountyCycleStart.toString(10) } },
Expiry: { $date: { $numberLong: bountyCycleEnd.toString(10) } },
Tag: "EntratiSyndicate",
Seed: bountyCycle,
Seed: seed,
Nodes: [],
Jobs: [
{
@ -1089,14 +1164,15 @@ export const getLiteSortie = (week: number): ILiteSortie => {
value.systemIndex === systemIndex &&
value.factionIndex !== undefined &&
value.factionIndex < 2 &&
value.name.indexOf("Archwing") == -1 &&
!isArchwingMission(value) &&
value.missionIndex != 0 // Exclude MT_ASSASSINATION
) {
nodes.push(key);
}
}
const rng = new CRng(week);
const seed = new CRng(week).randomInt(0, 0xffff);
const rng = new CRng(seed);
const firstNodeIndex = rng.randomInt(0, nodes.length - 1);
const firstNode = nodes[firstNodeIndex];
nodes.splice(firstNodeIndex, 1);
@ -1105,12 +1181,12 @@ export const getLiteSortie = (week: number): ILiteSortie => {
const weekEnd = weekStart + 604800000;
return {
_id: {
$oid: Math.trunc(weekStart / 1000).toString(16) + "5e23a244740a190c"
$oid: ((weekStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") + "5e23a244740a190c"
},
Activation: { $date: { $numberLong: weekStart.toString() } },
Expiry: { $date: { $numberLong: weekEnd.toString() } },
Reward: "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards",
Seed: week,
Seed: seed,
Boss: boss,
Missions: [
{
@ -1120,7 +1196,7 @@ export const getLiteSortie = (week: number): ILiteSortie => {
"MT_EXTERMINATION",
"MT_SABOTAGE",
"MT_RESCUE"
]),
])!,
node: firstNode
},
{
@ -1130,8 +1206,8 @@ export const getLiteSortie = (week: number): ILiteSortie => {
"MT_ARTIFACT",
"MT_EXCAVATE",
"MT_SURVIVAL"
]),
node: rng.randomElement(nodes)
])!,
node: rng.randomElement(nodes)!
},
{
missionType: "MT_ASSASSINATION",
@ -1140,3 +1216,14 @@ export const getLiteSortie = (week: number): ILiteSortie => {
]
};
};
export const isArchwingMission = (node: IRegion): boolean => {
if (node.name.indexOf("Archwing") != -1) {
return true;
}
// SettlementNode10
if (node.missionIndex == 25) {
return true;
}
return false;
};

View File

@ -11,6 +11,7 @@ export interface IGuildClient {
Members: IGuildMemberClient[];
Ranks: IGuildRank[];
Tier: number;
Emblem?: boolean;
Vault: IGuildVault;
ActiveDojoColorResearch: string;
Class: number;
@ -206,6 +207,7 @@ export interface IDojoDecoClient {
Type: string;
Pos: number[];
Rot: number[];
Scale?: number;
Name?: string; // for teleporters
Sockets?: number;
RegularCredits?: number;

View File

@ -43,6 +43,7 @@ export interface IInventoryDatabase
| "RecentVendorPurchases"
| "NextRefill"
| "Nemesis"
| "NemesisHistory"
| "EntratiVaultCountResetDate"
| "BrandedSuits"
| "LockedWeaponGroup"
@ -51,6 +52,7 @@ export interface IInventoryDatabase
| "LastLiteSortieReward"
| "CrewMembers"
| "QualifyingInvasions"
| "LastInventorySync"
| TEquipmentKey
>,
InventoryDatabaseEquipment {
@ -79,6 +81,7 @@ export interface IInventoryDatabase
RecentVendorPurchases?: IRecentVendorPurchaseDatabase[];
NextRefill?: Date;
Nemesis?: INemesisDatabase;
NemesisHistory?: INemesisBaseDatabase[];
EntratiVaultCountResetDate?: Date;
BrandedSuits?: Types.ObjectId[];
LockedWeaponGroup?: ILockedWeaponGroupDatabase;
@ -87,6 +90,7 @@ export interface IInventoryDatabase
LastLiteSortieReward?: ILastSortieRewardDatabase[];
CrewMembers: ICrewMemberDatabase[];
QualifyingInvasions: IInvasionProgressDatabase[];
LastInventorySync?: Types.ObjectId;
}
export interface IQuestKeyDatabase {
@ -134,7 +138,7 @@ export const equipmentKeys = [
export type TEquipmentKey = (typeof equipmentKeys)[number];
export interface IDuviriInfo {
Seed: number;
Seed: bigint;
NumCompletions: number;
}
@ -202,7 +206,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
Mailbox?: IMailboxClient;
SubscribedToEmails: number;
Created: IMongoDate;
RewardSeed: number | bigint;
RewardSeed: bigint;
RegularCredits: number;
PremiumCredits: number;
PremiumCreditsFree: number;
@ -256,7 +260,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
EquippedGear: string[];
DeathMarks: string[];
FusionTreasures: IFusionTreasure[];
WebFlags: IWebFlags;
//WebFlags: IWebFlags;
CompletedAlerts: string[];
Consumables: ITypeCount[];
LevelKeys: ITypeCount[];
@ -266,10 +270,10 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
KubrowPetEggs?: IKubrowPetEggClient[];
LoreFragmentScans: ILoreFragmentScan[];
EquippedEmotes: string[];
PendingTrades: IPendingTrade[];
//PendingTrades: IPendingTrade[];
Boosters: IBooster[];
ActiveDojoColorResearch: string;
SentientSpawnChanceBoosters: ISentientSpawnChanceBoosters;
//SentientSpawnChanceBoosters: ISentientSpawnChanceBoosters;
SupportedSyndicate?: string;
Affiliations: IAffiliation[];
QualifyingInvasions: IInvasionProgressClient[];
@ -291,19 +295,19 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
ActiveAvatarImageType: string;
ShipDecorations: ITypeCount[];
DiscoveredMarkers: IDiscoveredMarker[];
CompletedJobs: ICompletedJob[];
//CompletedJobs: ICompletedJob[];
FocusAbility?: string;
FocusUpgrades: IFocusUpgrade[];
HasContributedToDojo?: boolean;
HWIDProtectEnabled?: boolean;
KubrowPetPrints: IKubrowPetPrint[];
//KubrowPetPrints: IKubrowPetPrint[];
AlignmentReplay?: IAlignment;
PersonalGoalProgress: IPersonalGoalProgress[];
//PersonalGoalProgress: IPersonalGoalProgress[];
ThemeStyle: string;
ThemeBackground: string;
ThemeSounds: string;
BountyScore: number;
ChallengeInstanceStates: IChallengeInstanceState[];
//ChallengeInstanceStates: IChallengeInstanceState[];
LoginMilestoneRewards: string[];
RecentVendorPurchases?: IRecentVendorPurchaseClient[];
NodeIntrosCompleted: string[];
@ -311,37 +315,37 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
CompletedJobChains?: ICompletedJobChain[];
SeasonChallengeHistory: ISeasonChallenge[];
EquippedInstrument?: string;
InvasionChainProgress: IInvasionChainProgress[];
//InvasionChainProgress: IInvasionChainProgress[];
Nemesis?: INemesisClient;
NemesisHistory: INemesisBaseClient[];
LastNemesisAllySpawnTime?: IMongoDate;
NemesisHistory?: INemesisBaseClient[];
//LastNemesisAllySpawnTime?: IMongoDate;
Settings?: ISettings;
PersonalTechProjects: IPersonalTechProjectClient[];
PlayerSkills: IPlayerSkills;
CrewShipAmmo: ITypeCount[];
CrewShipWeaponSkins: IUpgradeClient[];
CrewShipSalvagedWeaponSkins: IUpgradeClient[];
TradeBannedUntil?: IMongoDate;
//TradeBannedUntil?: IMongoDate;
PlayedParkourTutorial: boolean;
SubscribedToEmailsPersonalized: number;
InfestedFoundry?: IInfestedFoundryClient;
BlessingCooldown?: IMongoDate;
CrewShipRawSalvage: ITypeCount[];
CrewMembers: ICrewMemberClient[];
LotusCustomization: ILotusCustomization;
LotusCustomization?: ILotusCustomization;
UseAdultOperatorLoadout?: boolean;
NemesisAbandonedRewards: string[];
LastInventorySync: IOid;
LastInventorySync?: IOid;
NextRefill?: IMongoDate;
FoundToday?: IMiscItem[]; // for Argon Crystals
CustomMarkers?: ICustomMarkers[];
ActiveLandscapeTraps: any[];
//ActiveLandscapeTraps: any[];
EvolutionProgress?: IEvolutionProgress[];
RepVotes: any[];
LeagueTickets: any[];
Quests: any[];
Robotics: any[];
UsedDailyDeals: any[];
//RepVotes: any[];
//LeagueTickets: any[];
//Quests: any[];
//Robotics: any[];
//UsedDailyDeals: any[];
LibraryPersonalTarget?: string;
LibraryPersonalProgress: ILibraryPersonalProgress[];
CollectibleSeries?: ICollectibleEntry[];
@ -353,7 +357,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
DeathSquadable: boolean;
EndlessXP?: IEndlessXpProgress[];
DialogueHistory?: IDialogueHistoryClient;
CalendarProgress: ICalendarProgress;
CalendarProgress?: ICalendarProgress;
SongChallenges?: ISongChallenge[];
EntratiVaultCountLastPeriod?: number;
EntratiVaultCountResetDate?: IMongoDate;
@ -845,7 +849,7 @@ export interface IMission extends IMissionDatabase {
}
export interface INemesisBaseClient {
fp: bigint;
fp: bigint | number;
manifest: string;
KillingSuit: string;
killingDamageType: number;
@ -863,7 +867,8 @@ export interface INemesisBaseClient {
Weakened: boolean;
}
export interface INemesisBaseDatabase extends Omit<INemesisBaseClient, "d"> {
export interface INemesisBaseDatabase extends Omit<INemesisBaseClient, "fp" | "d"> {
fp: bigint;
d: Date;
}
@ -877,7 +882,8 @@ export interface INemesisClient extends INemesisBaseClient {
LastEnc: number;
}
export interface INemesisDatabase extends Omit<INemesisClient, "d"> {
export interface INemesisDatabase extends Omit<INemesisClient, "fp" | "d"> {
fp: bigint;
d: Date;
}
@ -900,8 +906,8 @@ export interface IPendingRecipeDatabase {
ItemType: string;
CompletionDate: Date;
ItemId: IOid;
TargetItemId?: string; // likely related to liches
TargetFingerprint?: string; // likely related to liches
TargetItemId?: string; // unsure what this is for
TargetFingerprint?: string;
LongGuns?: IEquipmentDatabase[];
Pistols?: IEquipmentDatabase[];
Melee?: IEquipmentDatabase[];
@ -949,6 +955,17 @@ export interface ICrewShipComponentFingerprint extends IInnateDamageFingerprint
SubroutineIndex?: number;
}
export interface INemesisWeaponTargetFingerprint {
ItemType: string;
UpgradeFingerprint: IInnateDamageFingerprint;
Name: string;
}
export interface INemesisPetTargetFingerprint {
Parts: string[];
Name: string;
}
export enum GettingSlotOrderInfo {
Empty = "",
LotusUpgradesModsRandomizedPlayerMeleeWeaponRandomModRare0 = "/Lotus/Upgrades/Mods/Randomized/PlayerMeleeWeaponRandomModRare:0",
@ -1193,17 +1210,18 @@ export interface IMarker {
z: number;
showInHud: boolean;
}
export interface ISeasonProgress {
SeasonType: "CST_UNDEFINED" | "CST_WINTER" | "CST_SPRING" | "CST_SUMMER" | "CST_FALL";
SeasonType: "CST_WINTER" | "CST_SPRING" | "CST_SUMMER" | "CST_FALL";
LastCompletedDayIdx: number;
LastCompletedChallengeDayIdx: number;
ActivatedChallenges: unknown[];
ActivatedChallenges: string[];
}
export interface ICalendarProgress {
Version: number;
Iteration: number;
YearProgress: { Upgrades: unknown[] };
YearProgress: { Upgrades: string[] };
SeasonProgress: ISeasonProgress;
}

View File

@ -1,3 +1,5 @@
import { IAffiliationMods, IInventoryChanges } from "./purchaseTypes";
export const inventoryFields = ["RawUpgrades", "MiscItems", "Consumables", "Recipes"] as const;
export type IInventoryFieldType = (typeof inventoryFields)[number];
@ -6,8 +8,27 @@ export interface IMissionReward {
TypeName?: string;
UpgradeLevel?: number;
ItemCount: number;
DailyCooldown?: boolean;
Rarity?: number;
TweetText?: string;
ProductCategory?: string;
FromEnemyCache?: boolean;
IsStrippedItem?: boolean;
}
export interface IMissionCredits {
MissionCredits: number[];
CreditBonus: number[];
TotalCredits: number[];
DailyMissionBonus?: boolean;
}
export interface IMissionInventoryUpdateResponse extends Partial<IMissionCredits> {
ConquestCompletedMissionsCount?: number;
InventoryJson?: string;
MissionRewards?: IMissionReward[];
InventoryChanges?: IInventoryChanges;
FusionPoints?: number;
SyndicateXPItemReward?: number;
AffiliationMods?: IAffiliationMods[];
}

View File

@ -1,12 +1,12 @@
import { IColor } from "@/src/types/inventoryTypes/commonInventoryTypes";
import {
IApartment,
IRoom,
IPlacedDecosDatabase,
ITailorShop,
ITailorShopDatabase,
TBootLocation,
IApartmentDatabase
IApartmentDatabase,
IApartmentClient
} from "@/src/types/shipTypes";
import { Document, Model, Types } from "mongoose";
@ -21,10 +21,10 @@ export interface IOrbiter {
BootLocation?: TBootLocation;
}
export interface IPersonalRooms {
export interface IPersonalRoomsClient {
ShipInteriorColors: IColor;
Ship: IOrbiter;
Apartment: IApartment;
Apartment: IApartmentClient;
TailorShop: ITailorShop;
}

View File

@ -103,6 +103,7 @@ export const slotNames = [
"WeaponBin",
"MechBin",
"PveBonusLoadoutBin",
"PvpBonusLoadoutBin",
"SentinelBin",
"SpaceSuitBin",
"SpaceWeaponBin",

View File

@ -20,7 +20,10 @@ import {
IDiscoveredMarker,
ILockedWeaponGroupClient,
ILoadOutPresets,
IInvasionProgressClient
IInvasionProgressClient,
IWeaponSkinClient,
IKubrowPetEggClient,
INemesisClient
} from "./inventoryTypes/inventoryTypes";
import { IGroup } from "./loginTypes";
@ -44,6 +47,7 @@ export type IMissionInventoryUpdateRequest = {
SyndicateId?: string;
SortieId?: string;
CalendarProgress?: { challenge: string }[];
SeasonChallengeCompletions?: ISeasonChallenge[];
AffiliationChanges?: IAffiliationChange[];
crossPlaySetting?: string;
@ -71,6 +75,14 @@ export type IMissionInventoryUpdateRequest = {
PS: string;
ActiveDojoColorResearch: string;
RewardInfo?: IRewardInfo;
NemesisKillConvert?: {
nemesisName: string;
weaponLoc: string;
petLoc: "" | "/Lotus/Language/Pets/ZanukaPetName";
fingerprint: bigint | number;
killed: boolean;
};
target?: INemesisClient;
ReceivedCeremonyMsg: boolean;
LastCeremonyResetDate: number;
MissionPTS: number;
@ -100,6 +112,7 @@ export type IMissionInventoryUpdateRequest = {
}[];
CollectibleScans?: ICollectibleEntry[];
Upgrades?: IUpgradeClient[]; // riven challenge progress
WeaponSkins?: IWeaponSkinClient[];
StrippedItems?: {
DropTable: string;
DROP_MOD?: number[];
@ -115,6 +128,7 @@ export type IMissionInventoryUpdateRequest = {
NumExtraRewards: number;
Count: number;
}[];
KubrowPetEggs?: IKubrowPetEggClient[];
DiscoveredMarkers?: IDiscoveredMarker[];
LockedWeaponGroup?: ILockedWeaponGroupClient; // sent when captured by zanuka
UnlockWeapons?: boolean; // sent when recovered weapons from zanuka capture
@ -125,6 +139,16 @@ export type IMissionInventoryUpdateRequest = {
wagerTier?: number; // the index
creditsFee?: number; // the index
InvasionProgress?: IInvasionProgressClient[];
ConquestMissionsCompleted?: number;
duviriSuitSelection?: string;
duviriPistolSelection?: string;
duviriLongGunSelection?: string;
duviriMeleeSelection?: string;
duviriCaveOffers?: {
Seed: number | bigint;
Warframes: string[];
Weapons: string[];
};
} & {
[K in TEquipmentKey]?: IEquipmentClient[];
};
@ -134,7 +158,7 @@ export interface IRewardInfo {
invasionId?: string;
invasionAllyFaction?: "FC_GRINEER" | "FC_CORPUS";
sortieId?: string;
sortieTag?: string;
sortieTag?: "Mission1" | "Mission2" | "Final";
sortiePrereqs?: string[];
VaultsCracked?: number; // for Spy missions
rewardTier?: number;
@ -145,12 +169,19 @@ export interface IRewardInfo {
lostTargetWave?: number;
defenseTargetCount?: number;
NemesisAbandonedRewards?: string[];
NemesisHenchmenKills?: number;
NemesisHintProgress?: number;
EOM_AFK?: number;
rewardQualifications?: string; // did a Survival for 5 minutes and this was "1"
PurgatoryRewardQualifications?: string;
rewardSeed?: number | bigint;
periodicMissionTag?: string;
ConquestType?: string;
ConquestCompleted?: number;
ConquestEquipmentSuggestionsFulfilled?: number;
ConquestPersonalModifiersActive?: number;
ConquestStickersActive?: number;
ConquestHardModeActive?: number;
// for bounties, only EOM_AFK and node are given from above, plus:
JobTier?: number;
jobId?: string;

View File

@ -6,7 +6,8 @@ import {
ICrewShipMembersClient,
ICrewShipWeapon,
IFlavourItem,
ILoadoutConfigClient
ILoadoutConfigClient,
ILotusCustomization
} from "./inventoryTypes/inventoryTypes";
export interface ISaveLoadoutRequest {
@ -43,6 +44,7 @@ export interface ISaveLoadoutRequest {
EquippedEmotes: string[];
UseAdultOperatorLoadout: boolean;
WeaponSkins: IItemEntry;
LotusCustomization: ILotusCustomization;
}
export type ISaveLoadoutRequestNoUpgradeVer = Omit<ISaveLoadoutRequest, "UpgradeVer">;

View File

@ -1,12 +1,12 @@
import { Types } from "mongoose";
import { IOid } from "@/src/types/commonTypes";
import { IMongoDate, IOid } from "@/src/types/commonTypes";
import { IColor } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { ILoadoutClient } from "./saveLoadoutTypes";
export interface IGetShipResponse {
ShipOwnerId: string;
Ship: IShip;
Apartment: IApartment;
Apartment: IApartmentClient;
TailorShop: ITailorShop;
LoadOutInventory: { LoadOutPresets: ILoadoutClient };
}
@ -51,28 +51,42 @@ export interface IRoom {
PlacedDecos?: IPlacedDecosDatabase[];
}
export interface IPlants {
export interface IPlantClient {
PlantType: string;
EndTime: IOid;
EndTime: IMongoDate;
PlotIndex: number;
}
export interface IPlanters {
export interface IPlantDatabase extends Omit<IPlantClient, "EndTime"> {
EndTime: Date;
}
export interface IPlanterClient {
Name: string;
Plants: IPlants[];
Plants: IPlantClient[];
}
export interface IGardening {
Planters?: IPlanters[];
export interface IPlanterDatabase {
Name: string;
Plants: IPlantDatabase[];
}
export interface IApartment {
Gardening: IGardening;
export interface IGardeningClient {
Planters: IPlanterClient[];
}
export interface IGardeningDatabase {
Planters: IPlanterDatabase[];
}
export interface IApartmentClient {
Gardening: IGardeningClient;
Rooms: IRoom[];
FavouriteLoadouts: IFavouriteLoadout[];
}
export interface IApartmentDatabase {
Gardening: IGardening;
Gardening: IGardeningDatabase;
Rooms: IRoom[];
FavouriteLoadouts: IFavouriteLoadoutDatabase[];
}

View File

@ -1,18 +1,16 @@
import { IMongoDate, IOid } from "./commonTypes";
export interface IItemPrice {
ItemType: string | string[]; // If string[], preprocessing will use RNG to pick one for the current period.
ItemType: string;
ItemCount: number;
ProductCategory: string;
}
export interface IItemPricePreprocessed extends Omit<IItemPrice, "ItemType"> {
ItemType: string;
}
export interface IItemManifest {
StoreItem: string;
ItemPrices?: IItemPrice[];
RegularPrice?: number[];
PremiumPrice?: number[];
Bin: string;
QuantityMultiplier: number;
Expiry: IMongoDate; // Either a date in the distant future or a period in milliseconds for preprocessing.
@ -23,10 +21,6 @@ export interface IItemManifest {
Id: IOid;
}
export interface IItemManifestPreprocessed extends Omit<IItemManifest, "ItemPrices"> {
ItemPrices?: IItemPricePreprocessed[];
}
export interface IVendorInfo {
_id: IOid;
TypeName: string;
@ -38,14 +32,6 @@ export interface IVendorInfo {
Expiry: IMongoDate; // Either a date in the distant future or a period in milliseconds for preprocessing.
}
export interface IVendorInfoPreprocessed extends Omit<IVendorInfo, "ItemManifest"> {
ItemManifest: IItemManifestPreprocessed[];
}
export interface IRawVendorManifest {
export interface IVendorManifest {
VendorInfo: IVendorInfo;
}
export interface IVendorManifestPreprocessed {
VendorInfo: IVendorInfoPreprocessed;
}

View File

@ -97,6 +97,13 @@ export interface ISortie {
}[];
}
export interface ISortieMission {
missionType: string;
modifierType: string;
node: string;
tileset: string;
}
export interface ILiteSortie {
_id: IOid;
Activation: IMongoDate;
@ -126,7 +133,7 @@ export interface ISeasonChallenge {
export interface ICalendarSeason {
Activation: IMongoDate;
Expiry: IMongoDate;
Season: string; // "CST_UNDEFINED" | "CST_WINTER" | "CST_SPRING" | "CST_SUMMER" | "CST_FALL"
Season: "CST_WINTER" | "CST_SPRING" | "CST_SUMMER" | "CST_FALL";
Days: ICalendarDay[];
YearIteration: number;
Version: number;

View File

@ -0,0 +1,49 @@
[
"/Lotus/Weapons/ClanTech/Bio/BioWeapon",
"/Lotus/Weapons/ClanTech/Energy/EnergyRifle",
"/Lotus/Weapons/Corpus/Pistols/CorpusMinigun/CorpusMinigun",
"/Lotus/Weapons/Corpus/Pistols/CrpHandRL/CorpusHandRocketLauncher",
"/Lotus/Weapons/Grineer/LongGuns/GrineerSawbladeGun/SawBladeGun",
"/Lotus/Weapons/Grineer/Melee/GrineerTylAxeAndBoar/RegorAxeShield",
"/Lotus/Weapons/Grineer/Pistols/HeatGun/GrnHeatGun",
"/Lotus/Weapons/Infested/Pistols/InfVomitGun/InfVomitGunWep",
"/Lotus/Weapons/Syndicates/CephalonSuda/Pistols/CSDroidArray",
"/Lotus/Weapons/Tenno/Bows/HuntingBow",
"/Lotus/Weapons/Tenno/Bows/StalkerBow",
"/Lotus/Weapons/Tenno/LongGuns/TnoLeverAction/TnoLeverActionRifle",
"/Lotus/Weapons/Tenno/Melee/Axe/DualInfestedAxesWeapon",
"/Lotus/Weapons/Tenno/Melee/Dagger/CeramicDagger",
"/Lotus/Weapons/Tenno/Melee/Fist/Fist",
"/Lotus/Weapons/Tenno/Melee/Hammer/IceHammer/IceHammer",
"/Lotus/Weapons/Tenno/Melee/LongSword/LongSword",
"/Lotus/Weapons/Tenno/Melee/Maces/PaladinMace/PaladinMaceWeapon",
"/Lotus/Weapons/Tenno/Melee/Scythe/StalkerScytheWeapon",
"/Lotus/Weapons/Tenno/Melee/Scythe/ParisScythe/ParisScythe",
"/Lotus/Weapons/Tenno/Melee/Staff/Staff",
"/Lotus/Weapons/Tenno/Melee/Swords/CutlassAndPoignard/TennoCutlass",
"/Lotus/Weapons/Tenno/Melee/Swords/TennoSai/TennoSais",
"/Lotus/Weapons/Tenno/Pistol/AutoPistol",
"/Lotus/Weapons/Tenno/Pistol/BurstPistol",
"/Lotus/Weapons/Tenno/Pistol/HandShotGun",
"/Lotus/Weapons/Tenno/Pistol/HeavyPistol",
"/Lotus/Weapons/Tenno/Pistol/Pistol",
"/Lotus/Weapons/Tenno/Pistol/RevolverPistol",
"/Lotus/Weapons/Tenno/Pistols/ConclaveLeverPistol/ConclaveLeverPistol",
"/Lotus/Weapons/Tenno/Rifle/BoltoRifle",
"/Lotus/Weapons/Tenno/Rifle/BurstRifle",
"/Lotus/Weapons/Tenno/Rifle/HeavyRifle",
"/Lotus/Weapons/Tenno/Rifle/Rifle",
"/Lotus/Weapons/Tenno/Rifle/SemiAutoRifle",
"/Lotus/Weapons/Tenno/Rifle/TennoAR",
"/Lotus/Weapons/Tenno/Shotgun/FullAutoShotgun",
"/Lotus/Weapons/Tenno/Shotgun/Shotgun",
"/Lotus/Weapons/Tenno/ThrowingWeapons/Kunai",
"/Lotus/Weapons/Tenno/ThrowingWeapons/StalkerKunai",
"/Lotus/Weapons/Tenno/Zariman/LongGuns/PumpShotgun/ZarimanPumpShotgun",
"/Lotus/Weapons/Tenno/Zariman/LongGuns/SemiAutoRifle/ZarimanSemiAutoRifle",
"/Lotus/Weapons/Tenno/Zariman/Melee/Dagger/ZarimanDaggerWeapon",
"/Lotus/Weapons/Tenno/Zariman/Melee/Tonfas/ZarimanTonfaWeapon",
"/Lotus/Weapons/Tenno/Zariman/Pistols/HeavyPistol/ZarimanHeavyPistol",
"/Lotus/Weapons/Thanotech/EntFistIncarnon/EntFistIncarnon",
"/Lotus/Weapons/Thanotech/EntratiWristGun/EntratiWristGunWeapon"
]

View File

@ -1097,6 +1097,5 @@
"/Lotus/Types/LevelObjects/InfestedPumpkinCocoonLamp",
"/Lotus/Types/LevelObjects/InfestedPumpkinCocoonLampLarge",
"/Lotus/Types/LevelObjects/InfestedPumpkinCocoonLampSmall",
"/Lotus/Types/LevelObjects/InfestedPumpkinExplosiveTotem",
"/Lotus/Types/Enemies/Orokin/OrokinMoaBipedAvatar"
"/Lotus/Types/LevelObjects/InfestedPumpkinExplosiveTotem"
]

View File

@ -1,106 +0,0 @@
{
"VendorInfo": {
"_id": {
"$oid": "5f456e01c96976e97d6b8016"
},
"TypeName": "/Lotus/Types/Game/VendorManifests/Deimos/FishmongerVendorManifest",
"ItemManifest": [
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Deimos/FishParts/DeimosOrokinFishAPartItem",
"PremiumPrice": [9, 9],
"Bin": "BIN_1",
"QuantityMultiplier": 10,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "66fd60b20ba592c4c95e91b9"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Deimos/FishParts/DeimosInfestedFishDPartItem",
"PremiumPrice": [17, 17],
"Bin": "BIN_0",
"QuantityMultiplier": 20,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "66fd60b20ba592c4c95e91ba"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Deimos/FishParts/DeimosInfestedFishCPartItem",
"PremiumPrice": [10, 10],
"Bin": "BIN_1",
"QuantityMultiplier": 20,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "66fd60b20ba592c4c95e91bb"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Deimos/FishParts/DeimosInfestedFishBPartItem",
"PremiumPrice": [6, 6],
"Bin": "BIN_0",
"QuantityMultiplier": 20,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "66fd60b20ba592c4c95e91bc"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Deimos/FishParts/DeimosInfestedFishAPartItem",
"PremiumPrice": [5, 5],
"Bin": "BIN_0",
"QuantityMultiplier": 20,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "66fd60b20ba592c4c95e91bd"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Deimos/FishParts/DeimosGenericSharedFishPartItem",
"PremiumPrice": [7, 7],
"Bin": "BIN_0",
"QuantityMultiplier": 20,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "66fd60b20ba592c4c95e91be"
}
}
],
"PropertyTextHash": "6DF13A7FB573C25B4B4F989CBEFFC615",
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
}
}
}

View File

@ -1,101 +0,0 @@
{
"VendorInfo": {
"_id": { "$oid": "61ba123467e5d37975aeeb03" },
"TypeName": "/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest",
"ItemManifest": [
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementMoon",
"ItemPrices": [
{
"ItemType": ["/Lotus/Types/Items/Research/BioFragment", "/Lotus/Types/Items/Research/ChemComponent", "/Lotus/Types/Items/Research/EnergyFragment"],
"ItemCount": 12,
"ProductCategory": "MiscItems"
}
],
"RegularPrice": [1, 1],
"Bin": "BIN_4",
"QuantityMultiplier": 1,
"Expiry": { "$date": { "$numberLong": "604800000" } },
"PurchaseQuantityLimit": 1,
"AllowMultipurchase": false,
"LocTagRandSeed": 79554843,
"Id": { "$oid": "67bbb592e1534511d6c1c1e2" }
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementMountain",
"ItemPrices": [
{
"ItemType": ["/Lotus/Types/Items/Research/BioFragment", "/Lotus/Types/Items/Research/ChemComponent", "/Lotus/Types/Items/Research/EnergyFragment"],
"ItemCount": 7,
"ProductCategory": "MiscItems"
}
],
"RegularPrice": [1, 1],
"Bin": "BIN_3",
"QuantityMultiplier": 1,
"Expiry": { "$date": { "$numberLong": "604800000" } },
"PurchaseQuantityLimit": 1,
"AllowMultipurchase": false,
"LocTagRandSeed": 2413820225,
"Id": { "$oid": "67bbb592e1534511d6c1c1e3" }
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementStorm",
"ItemPrices": [
{
"ItemType": ["/Lotus/Types/Items/Research/BioFragment", "/Lotus/Types/Items/Research/ChemComponent", "/Lotus/Types/Items/Research/EnergyFragment"],
"ItemCount": 3,
"ProductCategory": "MiscItems"
}
],
"RegularPrice": [1, 1],
"Bin": "BIN_2",
"QuantityMultiplier": 1,
"Expiry": { "$date": { "$numberLong": "604800000" } },
"PurchaseQuantityLimit": 1,
"AllowMultipurchase": false,
"LocTagRandSeed": 3262300883,
"Id": { "$oid": "67bbb592e1534511d6c1c1e4" }
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementShadow",
"ItemPrices": [
{
"ItemType": ["/Lotus/Types/Items/Research/BioFragment", "/Lotus/Types/Items/Research/ChemComponent", "/Lotus/Types/Items/Research/EnergyFragment"],
"ItemCount": 20,
"ProductCategory": "MiscItems"
}
],
"RegularPrice": [1, 1],
"Bin": "BIN_1",
"QuantityMultiplier": 1,
"Expiry": { "$date": { "$numberLong": "604800000" } },
"PurchaseQuantityLimit": 1,
"AllowMultipurchase": false,
"LocTagRandSeed": 2797325750,
"Id": { "$oid": "67bbb592e1534511d6c1c1e5" }
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Guild/GuildAdvertisementGhost",
"ItemPrices": [
{
"ItemType": ["/Lotus/Types/Items/Research/BioFragment", "/Lotus/Types/Items/Research/ChemComponent", "/Lotus/Types/Items/Research/EnergyFragment"],
"ItemCount": 10,
"ProductCategory": "MiscItems"
}
],
"RegularPrice": [1, 1],
"Bin": "BIN_0",
"QuantityMultiplier": 1,
"Expiry": { "$date": { "$numberLong": "604800000" } },
"PurchaseQuantityLimit": 1,
"AllowMultipurchase": false,
"LocTagRandSeed": 554932310,
"Id": { "$oid": "67bbb592e1534511d6c1c1e6" }
}
],
"PropertyTextHash": "255AFE2169BAE4130B4B20D7C55D14FA",
"RandomSeedType": "VRST_FLAVOUR_TEXT",
"Expiry": { "$date": { "$numberLong": "604800000" } }
}
}

View File

@ -1,106 +0,0 @@
{
"VendorInfo": {
"_id": {
"$oid": "59d6e27ebcc718474eb17115"
},
"TypeName": "/Lotus/Types/Game/VendorManifests/Ostron/FishmongerVendorManifest",
"ItemManifest": [
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Eidolon/FishParts/DayUncommonFishAPartItem",
"PremiumPrice": [14, 14],
"Bin": "BIN_1",
"QuantityMultiplier": 10,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "66fd60b20ba592c4c95e9808"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Eidolon/FishParts/BothUncommonFishBPartItem",
"PremiumPrice": [12, 12],
"Bin": "BIN_1",
"QuantityMultiplier": 10,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "66fd60b20ba592c4c95e9809"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Eidolon/FishParts/DayCommonFishCPartItem",
"PremiumPrice": [8, 8],
"Bin": "BIN_0",
"QuantityMultiplier": 20,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "66fd60b20ba592c4c95e980a"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Eidolon/FishParts/DayCommonFishBPartItem",
"PremiumPrice": [7, 7],
"Bin": "BIN_0",
"QuantityMultiplier": 20,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "66fd60b20ba592c4c95e980b"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Eidolon/FishParts/DayCommonFishAPartItem",
"PremiumPrice": [10, 10],
"Bin": "BIN_0",
"QuantityMultiplier": 20,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "66fd60b20ba592c4c95e980c"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Eidolon/FishParts/BothCommonFishBPartItem",
"PremiumPrice": [8, 8],
"Bin": "BIN_0",
"QuantityMultiplier": 20,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "66fd60b20ba592c4c95e980d"
}
}
],
"PropertyTextHash": "CC3B9DAFB38F412998E90A41421A8986",
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
}
}
}

View File

@ -1,248 +0,0 @@
{
"VendorInfo": {
"_id": {
"$oid": "5be4a159b144f3cdf1c22efa"
},
"TypeName": "/Lotus/Types/Game/VendorManifests/Solaris/DebtTokenVendorManifest",
"ItemManifest": [
{
"StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleUncommonD",
"ItemPrices": [
{
"ItemType": "/Lotus/Types/Gameplay/Venus/Resources/VenusCoconutItem",
"ItemCount": 5,
"ProductCategory": "MiscItems"
},
{
"ItemType": "/Lotus/Types/Items/MiscItems/Circuits",
"ItemCount": 3664,
"ProductCategory": "MiscItems"
}
],
"RegularPrice": [87300, 87300],
"Bin": "BIN_1",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"PurchaseQuantityLimit": 1,
"AllowMultipurchase": true,
"LocTagRandSeed": 1881404827,
"Id": {
"$oid": "670daf92d21f34757a5e73b4"
}
},
{
"StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleRareC",
"ItemPrices": [
{
"ItemType": "/Lotus/Types/Items/MiscItems/NeuralSensor",
"ItemCount": 1,
"ProductCategory": "MiscItems"
}
],
"RegularPrice": [53300, 53300],
"Bin": "BIN_2",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"PurchaseQuantityLimit": 1,
"AllowMultipurchase": true,
"LocTagRandSeed": 1943984533,
"Id": {
"$oid": "6710b5029e1a3080a65e73a7"
}
},
{
"StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleCommonG",
"ItemPrices": [
{
"ItemType": "/Lotus/Types/Items/MiscItems/Salvage",
"ItemCount": 11540,
"ProductCategory": "MiscItems"
}
],
"RegularPrice": [27300, 27300],
"Bin": "BIN_0",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"PurchaseQuantityLimit": 1,
"AllowMultipurchase": true,
"LocTagRandSeed": 744199559,
"Id": {
"$oid": "67112582cc115756985e73a4"
}
},
{
"StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleUncommonB",
"ItemPrices": [
{
"ItemType": "/Lotus/Types/Items/Fish/Solaris/FishParts/CorpusFishThermalLaserItem",
"ItemCount": 9,
"ProductCategory": "MiscItems"
}
],
"RegularPrice": [75800, 75800],
"Bin": "BIN_1",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"PurchaseQuantityLimit": 1,
"AllowMultipurchase": true,
"LocTagRandSeed": 3744711432,
"Id": {
"$oid": "670de7d28a6ec82cd25e73a2"
}
},
{
"StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleUncommonB",
"ItemPrices": [
{
"ItemType": "/Lotus/Types/Items/MiscItems/Rubedo",
"ItemCount": 3343,
"ProductCategory": "MiscItems"
}
],
"RegularPrice": [52200, 52200],
"Bin": "BIN_1",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"PurchaseQuantityLimit": 1,
"AllowMultipurchase": true,
"LocTagRandSeed": 1579000687,
"Id": {
"$oid": "670e58526171148e125e73ad"
}
},
{
"StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleCommonA",
"ItemPrices": [
{
"ItemType": "/Lotus/Types/Gameplay/Venus/Resources/CoolantItem",
"ItemCount": 9,
"ProductCategory": "MiscItems"
},
{
"ItemType": "/Lotus/Types/Items/Fish/Solaris/FishParts/CorpusFishAnoscopicSensorItem",
"ItemCount": 5,
"ProductCategory": "MiscItems"
}
],
"RegularPrice": [12400, 12400],
"Bin": "BIN_0",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"PurchaseQuantityLimit": 1,
"AllowMultipurchase": true,
"LocTagRandSeed": 3589081466,
"Id": {
"$oid": "67112582cc115756985e73a5"
}
},
{
"StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleUncommonC",
"ItemPrices": [
{
"ItemType": "/Lotus/Types/Items/Gems/Solaris/SolarisCommonOreBAlloyItem",
"ItemCount": 13,
"ProductCategory": "MiscItems"
}
],
"RegularPrice": [77500, 77500],
"Bin": "BIN_1",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"PurchaseQuantityLimit": 1,
"AllowMultipurchase": true,
"LocTagRandSeed": 1510234814,
"Id": {
"$oid": "670f0f21250ad046c35e73ee"
}
},
{
"StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleUncommonD",
"ItemPrices": [
{
"ItemType": "/Lotus/Types/Items/Fish/Solaris/FishParts/CorpusFishParralelBiodeItem",
"ItemCount": 7,
"ProductCategory": "MiscItems"
},
{
"ItemType": "/Lotus/Types/Items/Gems/Solaris/SolarisCommonGemBCutItem",
"ItemCount": 12,
"ProductCategory": "MiscItems"
}
],
"RegularPrice": [94600, 94600],
"Bin": "BIN_1",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"PurchaseQuantityLimit": 1,
"AllowMultipurchase": true,
"LocTagRandSeed": 4222095721,
"Id": {
"$oid": "670f63827be40254f95e739d"
}
},
{
"StoreItem": "/Lotus/Types/StoreItems/Packages/DebtTokenBundles/DebtTokenBundleCommonJ",
"ItemPrices": [
{
"ItemType": "/Lotus/Types/Items/MiscItems/Nanospores",
"ItemCount": 14830,
"ProductCategory": "MiscItems"
}
],
"RegularPrice": [25600, 25600],
"Bin": "BIN_0",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"PurchaseQuantityLimit": 1,
"AllowMultipurchase": true,
"LocTagRandSeed": 2694388669,
"Id": {
"$oid": "67112582cc115756985e73a6"
}
}
],
"PropertyTextHash": "A39621049CA3CA13761028CD21C239EF",
"RandomSeedType": "VRST_FLAVOUR_TEXT",
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
}
}
}

View File

@ -1,106 +0,0 @@
{
"VendorInfo": {
"_id": {
"$oid": "5b0de8556df82a56ea9bae82"
},
"TypeName": "/Lotus/Types/Game/VendorManifests/Solaris/FishmongerVendorManifest",
"ItemManifest": [
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Solaris/FishParts/CorpusFishThermalLaserItem",
"PremiumPrice": [15, 15],
"Bin": "BIN_1",
"QuantityMultiplier": 10,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "66fd60b20ba592c4c95e9515"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Solaris/FishParts/CorpusFishVenedoCaseItem",
"PremiumPrice": [8, 8],
"Bin": "BIN_0",
"QuantityMultiplier": 20,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "66fd60b20ba592c4c95e9516"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Solaris/FishParts/SolarisFishDissipatorCoilItem",
"PremiumPrice": [18, 18],
"Bin": "BIN_0",
"QuantityMultiplier": 20,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "66fd60b20ba592c4c95e9517"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Solaris/FishParts/CorpusFishExaBrainItem",
"PremiumPrice": [5, 5],
"Bin": "BIN_0",
"QuantityMultiplier": 20,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "66fd60b20ba592c4c95e9518"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Solaris/FishParts/CorpusFishAnoscopicSensorItem",
"PremiumPrice": [5, 5],
"Bin": "BIN_0",
"QuantityMultiplier": 20,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "66fd60b20ba592c4c95e9519"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/Fish/Solaris/FishParts/GenericFishScrapItem",
"PremiumPrice": [5, 5],
"Bin": "BIN_0",
"QuantityMultiplier": 20,
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "66fd60b20ba592c4c95e951a"
}
}
],
"PropertyTextHash": "946131D0CF5CDF7C2C03BB967DE0DF49",
"Expiry": {
"$date": {
"$numberLong": "9999999000000"
}
}
}
}

View File

@ -0,0 +1,459 @@
{
"VendorInfo": {
"_id": {
"$oid": "67dadc30e4b6e0e5979c8d56"
},
"TypeName": "/Lotus/Types/Game/VendorManifests/TheHex/Temple1999VendorManifest",
"ItemManifest": [
{
"StoreItem": "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TempleBlueprint",
"ItemPrices": [
{
"ItemCount": 195,
"ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense",
"ProductCategory": "MiscItems"
}
],
"Bin": "BIN_0",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "2051240400000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "67dadc30641da66dc5c1c18c"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TempleSystemsBlueprint",
"ItemPrices": [
{
"ItemCount": 65,
"ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense",
"ProductCategory": "MiscItems"
}
],
"Bin": "BIN_0",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "2051240400000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "67dadc30641da66dc5c1c18d"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TempleChassisBlueprint",
"ItemPrices": [
{
"ItemCount": 65,
"ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense",
"ProductCategory": "MiscItems"
}
],
"Bin": "BIN_0",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "2051240400000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "67dadc30641da66dc5c1c18e"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Recipes/WarframeRecipes/TempleHelmetBlueprint",
"ItemPrices": [
{
"ItemCount": 65,
"ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense",
"ProductCategory": "MiscItems"
}
],
"Bin": "BIN_0",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "2051240400000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "67dadc30641da66dc5c1c18f"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Recipes/Weapons/1999EntHybridPistolBlueprint",
"ItemPrices": [
{
"ItemCount": 120,
"ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense",
"ProductCategory": "MiscItems"
}
],
"Bin": "BIN_0",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "2051240400000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "67dadc30641da66dc5c1c190"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Recipes/Weapons/WeaponParts/1999EntHybridPistolBarrelBlueprint",
"ItemPrices": [
{
"ItemCount": 60,
"ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense",
"ProductCategory": "MiscItems"
}
],
"Bin": "BIN_0",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "2051240400000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "67dadc30641da66dc5c1c191"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Recipes/Weapons/WeaponParts/1999EntHybridPistolReceiverBlueprint",
"ItemPrices": [
{
"ItemCount": 60,
"ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense",
"ProductCategory": "MiscItems"
}
],
"Bin": "BIN_0",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "2051240400000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "67dadc30641da66dc5c1c192"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Recipes/Weapons/WeaponParts/1999EntHybridPistolStockBlueprint",
"ItemPrices": [
{
"ItemCount": 60,
"ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense",
"ProductCategory": "MiscItems"
}
],
"Bin": "BIN_0",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "2051240400000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "67dadc30641da66dc5c1c193"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Hollvania/LASxStadiumDrumCoreKitA",
"ItemPrices": [
{
"ItemCount": 30,
"ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense",
"ProductCategory": "MiscItems"
}
],
"Bin": "BIN_0",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "2051240400000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "67dadc30641da66dc5c1c194"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Hollvania/LASxStadiumDrumCymbalA",
"ItemPrices": [
{
"ItemCount": 30,
"ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense",
"ProductCategory": "MiscItems"
}
],
"Bin": "BIN_0",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "2051240400000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "67dadc30641da66dc5c1c195"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Hollvania/LASxStadiumDrumFloorTomA",
"ItemPrices": [
{
"ItemCount": 30,
"ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense",
"ProductCategory": "MiscItems"
}
],
"Bin": "BIN_0",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "2051240400000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "67dadc30641da66dc5c1c196"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Hollvania/LASxStadiumDrumSnareA",
"ItemPrices": [
{
"ItemCount": 30,
"ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense",
"ProductCategory": "MiscItems"
}
],
"Bin": "BIN_0",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "2051240400000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "67dadc30641da66dc5c1c197"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Hollvania/LASxStadiumEquipmentCaseA",
"ItemPrices": [
{
"ItemCount": 30,
"ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense",
"ProductCategory": "MiscItems"
}
],
"Bin": "BIN_0",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "2051240400000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "67dadc30641da66dc5c1c198"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Hollvania/LASxStadiumEquipmentCaseB",
"ItemPrices": [
{
"ItemCount": 30,
"ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense",
"ProductCategory": "MiscItems"
}
],
"Bin": "BIN_0",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "2051240400000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "67dadc30641da66dc5c1c199"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Hollvania/LASxStadiumEquipmentCaseC",
"ItemPrices": [
{
"ItemCount": 30,
"ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense",
"ProductCategory": "MiscItems"
}
],
"Bin": "BIN_0",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "2051240400000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "67dadc30641da66dc5c1c19a"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Hollvania/LASxStadiumEquipmentCaseD",
"ItemPrices": [
{
"ItemCount": 30,
"ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense",
"ProductCategory": "MiscItems"
}
],
"Bin": "BIN_0",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "2051240400000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "67dadc30641da66dc5c1c19b"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Hollvania/LASxStadiumEquipmentCaseE",
"ItemPrices": [
{
"ItemCount": 30,
"ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense",
"ProductCategory": "MiscItems"
}
],
"Bin": "BIN_0",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "2051240400000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "67dadc30641da66dc5c1c19c"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Hollvania/LASxStadiumEquipmentCaseF",
"ItemPrices": [
{
"ItemCount": 30,
"ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense",
"ProductCategory": "MiscItems"
}
],
"Bin": "BIN_0",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "2051240400000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "67dadc30641da66dc5c1c19d"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/Hollvania/LASxStadiumSynthKeyboardA",
"ItemPrices": [
{
"ItemCount": 30,
"ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense",
"ProductCategory": "MiscItems"
}
],
"Bin": "BIN_0",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "2051240400000"
}
},
"AllowMultipurchase": true,
"Id": {
"$oid": "67dadc30641da66dc5c1c19e"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/PhotoBooth/Vania/PhotoboothTileVaniaObjTempleDefense",
"ItemPrices": [
{
"ItemCount": 100,
"ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense",
"ProductCategory": "MiscItems"
}
],
"Bin": "BIN_0",
"QuantityMultiplier": 1,
"Expiry": {
"$date": {
"$numberLong": "2051240400000"
}
},
"AllowMultipurchase": false,
"Id": {
"$oid": "67dadc30641da66dc5c1c19f"
}
},
{
"StoreItem": "/Lotus/StoreItems/Types/Items/MiscItems/Kuva",
"ItemPrices": [
{
"ItemCount": 110,
"ItemType": "/Lotus/Types/Gameplay/1999Wf/Resources/1999ResourceDefense",
"ProductCategory": "MiscItems"
}
],
"Bin": "BIN_0",
"QuantityMultiplier": 6000,
"Expiry": {
"$date": {
"$numberLong": "2051240400000"
}
},
"PurchaseQuantityLimit": 7,
"AllowMultipurchase": true,
"Id": {
"$oid": "67dadc30641da66dc5c1c1a5"
}
}
],
"PropertyTextHash": "20B13D9EB78FEC80EA32D0687F5BA1AE",
"RequiredGoalTag": "",
"Expiry": {
"$date": {
"$numberLong": "2051240400000"
}
}
}
}

View File

@ -0,0 +1,175 @@
{
"SettlementNode1": "CorpusShipTileset",
"SettlementNode11": "CorpusShipTileset",
"SettlementNode12": "CorpusShipTileset",
"SettlementNode14": "CorpusShipTileset",
"SettlementNode15": "CorpusShipTileset",
"SettlementNode2": "CorpusShipTileset",
"SettlementNode20": "CorpusShipTileset",
"SettlementNode3": "CorpusShipTileset",
"SolNode1": "CorpusOutpostTileset",
"SolNode10": "CorpusGasCityTileset",
"SolNode100": "CorpusGasCityTileset",
"SolNode101": "CorpusOutpostTileset",
"SolNode102": "CorpusShipTileset",
"SolNode103": "GrineerAsteroidTileset",
"SolNode104": "CorpusShipTileset",
"SolNode105": "GrineerOceanTilesetAnywhere",
"SolNode106": "GrineerSettlementTileset",
"SolNode107": "CorpusOutpostTileset",
"SolNode108": "GrineerAsteroidTileset",
"SolNode109": "CorpusOutpostTileset",
"SolNode11": "GrineerSettlementTileset",
"SolNode113": "GrineerSettlementTileset",
"SolNode118": "CorpusShipTileset",
"SolNode119": "GrineerAsteroidTileset",
"SolNode12": "GrineerAsteroidTileset",
"SolNode121": "CorpusGasCityTileset",
"SolNode122": "GrineerOceanTileset",
"SolNode123": "CorpusShipTileset",
"SolNode125": "CorpusGasCityTileset",
"SolNode126": "CorpusGasCityTileset",
"SolNode127": "CorpusShipTileset",
"SolNode128": "CorpusOutpostTileset",
"SolNode130": "GrineerAsteroidTileset",
"SolNode131": "GrineerShipyardsTileset",
"SolNode132": "GrineerShipyardsTileset",
"SolNode135": "GrineerGalleonTileset",
"SolNode137": "GrineerShipyardsTileset",
"SolNode138": "GrineerShipyardsTileset",
"SolNode139": "GrineerShipyardsTileset",
"SolNode14": "CorpusIcePlanetTilesetCaves",
"SolNode140": "GrineerShipyardsTileset",
"SolNode141": "GrineerShipyardsTileset",
"SolNode144": "GrineerShipyardsTileset",
"SolNode146": "GrineerAsteroidTileset",
"SolNode147": "GrineerShipyardsTileset",
"SolNode149": "GrineerShipyardsTileset",
"SolNode15": "GrineerGalleonTileset",
"SolNode16": "GrineerSettlementTileset",
"SolNode162": "InfestedCorpusShipTileset",
"SolNode164": "InfestedCorpusShipTileset",
"SolNode166": "InfestedCorpusShipTileset",
"SolNode17": "CorpusShipTileset",
"SolNode171": "InfestedCorpusShipTileset",
"SolNode172": "CorpusShipTileset",
"SolNode173": "InfestedCorpusShipTileset",
"SolNode175": "InfestedCorpusShipTileset",
"SolNode177": "GrineerGalleonTileset",
"SolNode18": "GrineerAsteroidTileset",
"SolNode181": "GrineerAsteroidTileset",
"SolNode184": "GrineerGalleonTileset",
"SolNode185": "GrineerGalleonTileset",
"SolNode187": "GrineerAsteroidTileset",
"SolNode188": "GrineerGalleonTileset",
"SolNode189": "GrineerGalleonTileset",
"SolNode19": "GrineerAsteroidTileset",
"SolNode191": "GrineerShipyardsTileset",
"SolNode193": "GrineerAsteroidTileset",
"SolNode195": "GrineerGalleonTileset",
"SolNode196": "GrineerGalleonTileset",
"SolNode2": "CorpusOutpostTileset",
"SolNode20": "GrineerGalleonTileset",
"SolNode203": "CorpusIcePlanetTileset",
"SolNode205": "CorpusIcePlanetTileset",
"SolNode209": "CorpusIcePlanetTileset",
"SolNode21": "CorpusOutpostTileset",
"SolNode210": "CorpusIcePlanetTileset",
"SolNode211": "CorpusIcePlanetTileset",
"SolNode212": "CorpusIcePlanetTileset",
"SolNode214": "CorpusIcePlanetTileset",
"SolNode215": "CorpusShipTileset",
"SolNode216": "CorpusIcePlanetTileset",
"SolNode217": "CorpusIcePlanetTileset",
"SolNode22": "CorpusOutpostTileset",
"SolNode220": "CorpusIcePlanetTileset",
"SolNode223": "GrineerAsteroidTileset",
"SolNode224": "GrineerGalleonTileset",
"SolNode225": "GrineerGalleonTileset",
"SolNode226": "GrineerGalleonTileset",
"SolNode228": "EidolonTileset",
"SolNode23": "CorpusShipTileset",
"SolNode24": "GrineerForestTileset",
"SolNode25": "CorpusGasCityTileset",
"SolNode26": "GrineerForestTileset",
"SolNode30": "GrineerSettlementTileset",
"SolNode300": "OrokinMoonTilesetGrineer",
"SolNode301": "OrokinMoonTilesetGrineer",
"SolNode302": "OrokinMoonTilesetCorpus",
"SolNode304": "OrokinMoonTilesetCorpus",
"SolNode305": "OrokinMoonTilesetGrineer",
"SolNode306": "OrokinMoonTilesetCorpus",
"SolNode307": "OrokinMoonTilesetCorpus",
"SolNode308": "OrokinMoonTilesetCorpus",
"SolNode31": "GrineerGalleonTileset",
"SolNode32": "GrineerGalleonTileset",
"SolNode36": "GrineerSettlementTileset",
"SolNode38": "CorpusOutpostTileset",
"SolNode39": "GrineerForestTileset",
"SolNode4": "CorpusShipTileset",
"SolNode400": "OrokinVoidTileset",
"SolNode401": "OrokinVoidTileset",
"SolNode402": "OrokinVoidTileset",
"SolNode403": "OrokinVoidTileset",
"SolNode404": "OrokinVoidTileset",
"SolNode405": "OrokinVoidTileset",
"SolNode406": "OrokinVoidTileset",
"SolNode407": "OrokinVoidTileset",
"SolNode408": "OrokinVoidTileset",
"SolNode409": "OrokinVoidTileset",
"SolNode41": "GrineerSettlementTileset",
"SolNode410": "OrokinVoidTileset",
"SolNode412": "OrokinVoidTileset",
"SolNode42": "GrineerGalleonTileset",
"SolNode43": "CorpusOutpostTileset",
"SolNode45": "GrineerSettlementTileset",
"SolNode46": "GrineerSettlementTileset",
"SolNode48": "CorpusOutpostTileset",
"SolNode49": "CorpusShipTileset",
"SolNode50": "GrineerAsteroidTileset",
"SolNode51": "CorpusOutpostTileset",
"SolNode53": "CorpusGasCityTileset",
"SolNode56": "CorpusShipTileset",
"SolNode57": "CorpusOutpostTileset",
"SolNode58": "GrineerSettlementTileset",
"SolNode59": "GrineerForestTileset",
"SolNode6": "CorpusOutpostTileset",
"SolNode61": "CorpusShipTileset",
"SolNode62": "CorpusIcePlanetTilesetCaves",
"SolNode64": "GrineerOceanTileset",
"SolNode66": "CorpusOutpostTileset",
"SolNode67": "GrineerAsteroidTileset",
"SolNode68": "GrineerGalleonTileset",
"SolNode70": "GrineerGalleonTileset",
"SolNode706": "OrokinDerelictTileset",
"SolNode707": "OrokinDerelictTileset",
"SolNode708": "OrokinDerelictTileset",
"SolNode709": "OrokinDerelictTileset",
"SolNode710": "OrokinDerelictTileset",
"SolNode711": "OrokinDerelictTileset",
"SolNode712": "OrokinDerelictTileset",
"SolNode713": "OrokinDerelictTileset",
"SolNode72": "CorpusOutpostTileset",
"SolNode73": "CorpusGasCityTileset",
"SolNode74": "CorpusGasCityTileset",
"SolNode741": "GrineerFortressTileset",
"SolNode742": "GrineerFortressTileset",
"SolNode743": "GrineerFortressTileset",
"SolNode744": "GrineerFortressTileset",
"SolNode745": "GrineerFortressTileset",
"SolNode746": "GrineerFortressTileset",
"SolNode747": "GrineerFortressTileset",
"SolNode748": "GrineerFortressTileset",
"SolNode75": "GrineerForestTileset",
"SolNode76": "CorpusShipTileset",
"SolNode78": "CorpusShipTileset",
"SolNode79": "GrineerForestTileset",
"SolNode81": "CorpusShipTileset",
"SolNode82": "GrineerGalleonTileset",
"SolNode84": "CorpusIcePlanetTilesetCaves",
"SolNode88": "CorpusShipTileset",
"SolNode93": "GrineerAsteroidTileset",
"SolNode96": "GrineerGalleonTileset",
"SolNode97": "CorpusGasCityTileset",
"SolNode99": "GrineerSettlementTileset"
}

View File

@ -0,0 +1,157 @@
[
"SettlementNode1",
"SettlementNode11",
"SettlementNode12",
"SettlementNode14",
"SettlementNode15",
"SettlementNode2",
"SettlementNode3",
"SolNode1",
"SolNode10",
"SolNode100",
"SolNode101",
"SolNode102",
"SolNode103",
"SolNode106",
"SolNode107",
"SolNode109",
"SolNode11",
"SolNode113",
"SolNode118",
"SolNode119",
"SolNode12",
"SolNode121",
"SolNode122",
"SolNode123",
"SolNode125",
"SolNode126",
"SolNode128",
"SolNode130",
"SolNode131",
"SolNode132",
"SolNode135",
"SolNode137",
"SolNode138",
"SolNode139",
"SolNode14",
"SolNode140",
"SolNode141",
"SolNode146",
"SolNode147",
"SolNode149",
"SolNode15",
"SolNode153",
"SolNode16",
"SolNode162",
"SolNode164",
"SolNode166",
"SolNode167",
"SolNode17",
"SolNode171",
"SolNode172",
"SolNode173",
"SolNode175",
"SolNode177",
"SolNode18",
"SolNode181",
"SolNode184",
"SolNode185",
"SolNode187",
"SolNode188",
"SolNode189",
"SolNode19",
"SolNode191",
"SolNode195",
"SolNode196",
"SolNode2",
"SolNode20",
"SolNode203",
"SolNode204",
"SolNode205",
"SolNode209",
"SolNode21",
"SolNode211",
"SolNode212",
"SolNode214",
"SolNode215",
"SolNode216",
"SolNode217",
"SolNode22",
"SolNode220",
"SolNode223",
"SolNode224",
"SolNode225",
"SolNode226",
"SolNode23",
"SolNode25",
"SolNode26",
"SolNode27",
"SolNode30",
"SolNode31",
"SolNode36",
"SolNode38",
"SolNode39",
"SolNode4",
"SolNode400",
"SolNode401",
"SolNode402",
"SolNode403",
"SolNode404",
"SolNode405",
"SolNode406",
"SolNode407",
"SolNode408",
"SolNode409",
"SolNode41",
"SolNode410",
"SolNode412",
"SolNode42",
"SolNode43",
"SolNode45",
"SolNode46",
"SolNode48",
"SolNode49",
"SolNode50",
"SolNode56",
"SolNode57",
"SolNode58",
"SolNode59",
"SolNode6",
"SolNode61",
"SolNode62",
"SolNode63",
"SolNode64",
"SolNode66",
"SolNode67",
"SolNode68",
"SolNode70",
"SolNode706",
"SolNode707",
"SolNode708",
"SolNode709",
"SolNode710",
"SolNode711",
"SolNode72",
"SolNode73",
"SolNode74",
"SolNode741",
"SolNode742",
"SolNode743",
"SolNode744",
"SolNode745",
"SolNode746",
"SolNode748",
"SolNode75",
"SolNode76",
"SolNode78",
"SolNode79",
"SolNode81",
"SolNode82",
"SolNode84",
"SolNode85",
"SolNode88",
"SolNode89",
"SolNode93",
"SolNode96",
"SolNode97"
]

View File

@ -401,6 +401,22 @@
</div>
</div>
</div>
<div class="row g-3">
<div class="col-lg-6">
<div class="card mb-3" style="height: 400px;">
<h5 class="card-header" data-loc="inventory_evolutionProgress"></h5>
<div class="card-body overflow-auto">
<form class="input-group mb-3" onsubmit="doAcquireEvolution();return false;">
<input class="form-control" id="acquire-type-EvolutionProgress" list="datalist-EvolutionProgress" />
<button class="btn btn-primary" type="submit" data-loc="general_addButton"></button>
</form>
<table class="table table-hover w-100">
<tbody id="EvolutionProgress-list"></tbody>
</table>
</div>
</div>
</div>
</div>
<div class="card mb-3">
<h5 class="card-header" data-loc="general_bulkActions"></h5>
<div class="card-body">
@ -411,6 +427,7 @@
<button class="btn btn-primary" onclick="addMissingEquipment(['SpaceGuns', 'SpaceMelee']);" data-loc="inventory_bulkAddSpaceWeapons"></button>
<button class="btn btn-primary" onclick="addMissingEquipment(['Sentinels']);" data-loc="inventory_bulkAddSentinels"></button>
<button class="btn btn-primary" onclick="addMissingEquipment(['SentinelWeapons']);" data-loc="inventory_bulkAddSentinelWeapons"></button>
<button class="btn btn-primary" onclick="addMissingEvolutionProgress();" data-loc="inventory_bulkAddEvolutionProgress"></button>
</div>
<div class="mb-2 d-flex flex-wrap gap-2">
<button class="btn btn-success" onclick="maxRankAllEquipment(['Suits']);" data-loc="inventory_bulkRankUpSuits"></button>
@ -419,6 +436,7 @@
<button class="btn btn-success" onclick="maxRankAllEquipment(['SpaceGuns', 'SpaceMelee']);" data-loc="inventory_bulkRankUpSpaceWeapons"></button>
<button class="btn btn-success" onclick="maxRankAllEquipment(['Sentinels']);" data-loc="inventory_bulkRankUpSentinels"></button>
<button class="btn btn-success" onclick="maxRankAllEquipment(['SentinelWeapons']);" data-loc="inventory_bulkRankUpSentinelWeapons"></button>
<button class="btn btn-success" onclick="maxRankAllEvolutions();" data-loc="inventory_bulkRankUpEvolutionProgress"></button>
</div>
</div>
</div>
@ -492,8 +510,9 @@
<div class="card mb-3">
<h5 class="card-header" data-loc="general_bulkActions"></h5>
<div class="card-body d-flex flex-wrap gap-2">
<button class="btn btn-primary" onclick="doAddAllMods();" data-loc="mods_bulkAddMods"></button>
<button class="btn btn-primary" onclick="doAddAllMods();" data-loc="mods_addMissingUnrankedMods"></button>
<button class="btn btn-danger" onclick="doRemoveUnrankedMods();" data-loc="mods_removeUnranked"></button>
<button class="btn btn-primary" onclick="doAddMissingMaxRankMods();" data-loc="mods_addMissingMaxRankMods"></button>
</div>
</div>
</div>
@ -575,6 +594,10 @@
<input class="form-check-input" type="checkbox" id="infiniteHelminthMaterials" />
<label class="form-check-label" for="infiniteHelminthMaterials" data-loc="cheats_infiniteHelminthMaterials"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="dontSubtractConsumables" />
<label class="form-check-label" for="dontSubtractConsumables" data-loc="cheats_dontSubtractConsumables"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="unlockAllShipFeatures" />
<label class="form-check-label" for="unlockAllShipFeatures" data-loc="cheats_unlockAllShipFeatures"></label>
@ -651,6 +674,10 @@
<input class="form-check-input" type="checkbox" id="noResourceExtractorDronesDamage" />
<label class="form-check-label" for="noResourceExtractorDronesDamage" data-loc="cheats_noResourceExtractorDronesDamage"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="skipClanKeyCrafting" />
<label class="form-check-label" for="skipClanKeyCrafting" data-loc="cheats_skipClanKeyCrafting"></label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="noDojoRoomBuildStage" />
<label class="form-check-label" for="noDojoRoomBuildStage" data-loc="cheats_noDojoRoomBuildStage"></label>
@ -733,6 +760,7 @@
</datalist>
<datalist id="datalist-archonCrystalUpgrades"></datalist>
<datalist id="datalist-OperatorAmps"></datalist>
<datalist id="datalist-EvolutionProgress"></datalist>
<datalist id="datalist-ModularParts"></datalist>
<datalist id="datalist-ModularParts-HB_DECK"></datalist>
<datalist id="datalist-ModularParts-HB_ENGINE"></datalist>

View File

@ -13,6 +13,7 @@ function doLogin() {
}
function loginFromLocalStorage() {
const isRegister = registerSubmit;
doLoginRequest(
data => {
if (single.getCurrentPath() == "/webui/") {
@ -28,7 +29,7 @@ function loginFromLocalStorage() {
},
() => {
logout();
alert("Login failed");
alert(isRegister ? "Registration failed. Account already exists?" : "Login failed");
}
);
}
@ -44,7 +45,7 @@ function doLoginRequest(succ_cb, fail_cb) {
s: "W0RFXVN0ZXZlIGxpa2VzIGJpZyBidXR0cw==", // signature of some kind
lang: "en",
date: 1501230947855458660, // ???
ClientType: registerSubmit ? "" : "webui",
ClientType: registerSubmit ? "webui-register" : "webui",
PS: "W0RFXVN0ZXZlIGxpa2VzIGJpZyBidXR0cw==" // anti-cheat data
})
});
@ -77,19 +78,23 @@ function logout() {
function renameAccount() {
const newname = window.prompt(loc("code_changeNameConfirm"));
if (newname) {
revalidateAuthz(() => {
fetch("/custom/renameAccount?" + window.authz + "&newname=" + newname).then(() => {
$(".displayname").text(newname);
updateLocElements();
});
});
}
}
function deleteAccount() {
if (window.confirm(loc("code_deleteAccountConfirm"))) {
revalidateAuthz(() => {
fetch("/custom/deleteAccount?" + window.authz).then(() => {
logout();
single.loadRoute("/webui/"); // Show login screen
});
});
}
}
@ -182,6 +187,16 @@ const webUiModularWeapons = [
"/Lotus/Types/Friendly/Pets/CreaturePets/MedjayPredatorKubrowPetPowerSuit"
];
const permanentEvolutionWeapons = new Set([
"/Lotus/Weapons/Tenno/Zariman/LongGuns/PumpShotgun/ZarimanPumpShotgun",
"/Lotus/Weapons/Tenno/Zariman/LongGuns/SemiAutoRifle/ZarimanSemiAutoRifle",
"/Lotus/Weapons/Tenno/Zariman/Melee/Dagger/ZarimanDaggerWeapon",
"/Lotus/Weapons/Tenno/Zariman/Melee/Tonfas/ZarimanTonfaWeapon",
"/Lotus/Weapons/Tenno/Zariman/Pistols/HeavyPistol/ZarimanHeavyPistol",
"/Lotus/Weapons/Thanotech/EntFistIncarnon/EntFistIncarnon",
"/Lotus/Weapons/Thanotech/EntratiWristGun/EntratiWristGunWeapon"
]);
let uniqueLevelCaps = {};
function fetchItemList() {
window.itemListPromise = new Promise(resolve => {
@ -360,6 +375,7 @@ function fetchItemList() {
}
fetchItemList();
// Assumes that caller revalidates authz
function updateInventory() {
const req = $.get("/api/inventory.php?" + window.authz + "&xpBasedLevelCapDisabled=1");
req.done(data => {
@ -472,6 +488,7 @@ function updateInventory() {
a.href = "#";
a.onclick = function (event) {
event.preventDefault();
revalidateAuthz(() => {
if (item.XP < maxXP) {
addGearExp(category, item.ItemId.$oid, maxXP - item.XP);
}
@ -491,6 +508,7 @@ function updateInventory() {
}
}
}
});
};
a.title = loc("code_maxRank");
a.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M214.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 141.2V448c0 17.7 14.3 32 32 32s32-14.3 32-32V141.2L329.4 246.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160z"/></svg>`;
@ -562,6 +580,82 @@ function updateInventory() {
});
});
document.getElementById("EvolutionProgress-list").innerHTML = "";
data.EvolutionProgress?.forEach(item => {
const datalist = document.getElementById("datalist-EvolutionProgress");
const optionToRemove = datalist.querySelector(`option[data-key="${item.ItemType}"]`);
if (optionToRemove) {
datalist.removeChild(optionToRemove);
}
const tr = document.createElement("tr");
tr.setAttribute("data-item-type", item.ItemType);
{
const td = document.createElement("td");
td.textContent = itemMap[item.ItemType]?.name ?? item.ItemType;
if (item.Rank != null) {
td.textContent += " | " + loc("code_rank") + ": [" + item.Rank + "/5]";
}
tr.appendChild(td);
}
{
const td = document.createElement("td");
td.classList = "text-end text-nowrap";
if (item.Rank < 5) {
const a = document.createElement("a");
a.href = "#";
a.onclick = function (event) {
event.preventDefault();
setEvolutionProgress([{ ItemType: item.ItemType, Rank: 5 }]);
};
a.title = loc("code_maxRank");
a.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--!Font Awesome Free 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M214.6 41.4c-12.5-12.5-32.8-12.5-45.3 0l-160 160c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 141.2V448c0 17.7 14.3 32 32 32s32-14.3 32-32V141.2L329.4 246.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-160-160z"/></svg>`;
td.appendChild(a);
}
if ((permanentEvolutionWeapons.has(item.ItemType) && item.Rank > 0) || item.Rank > 1) {
const a = document.createElement("a");
a.href = "#";
a.onclick = function (event) {
event.preventDefault();
setEvolutionProgress([{ ItemType: item.ItemType, Rank: item.Rank - 1 }]);
};
a.title = loc("code_rankDown");
a.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M233.4 406.6c12.5 12.5 32.8 12.5 45.3 0l192-192c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L256 338.7 86.6 169.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l192 192z"/></svg>`;
td.appendChild(a);
}
if (item.Rank < 5) {
const a = document.createElement("a");
a.href = "#";
a.onclick = function (event) {
event.preventDefault();
setEvolutionProgress([{ ItemType: item.ItemType, Rank: item.Rank + 1 }]);
};
a.title = loc("code_rankUp");
a.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M233.4 105.4c12.5-12.5 32.8-12.5 45.3 0l192 192c12.5 12.5 12.5 32.8 0 45.3s-32.8 12.5-45.3 0L256 173.3 86.6 342.6c-12.5 12.5-32.8 12.5-45.3 0s-12.5-32.8 0-45.3l192-192z"/></svg>`;
td.appendChild(a);
}
tr.appendChild(td);
}
document.getElementById("EvolutionProgress-list").appendChild(tr);
});
const datalistEvolutionProgress = document.querySelectorAll("#datalist-EvolutionProgress option");
const formEvolutionProgress = document.querySelector('form[onsubmit*="doAcquireEvolution()"]');
const giveAllQEvolutionProgress = document.querySelector(
'button[onclick*="addMissingEvolutionProgress()"]'
);
if (datalistEvolutionProgress.length === 0) {
formEvolutionProgress.classList.add("disabled");
formEvolutionProgress.querySelector("input").disabled = true;
formEvolutionProgress.querySelector("button").disabled = true;
giveAllQEvolutionProgress.disabled = true;
}
// Populate quests route
document.getElementById("QuestKeys-list").innerHTML = "";
data.QuestKeys.forEach(item => {
@ -673,18 +767,18 @@ function updateInventory() {
});
const datalistQuestKeys = document.querySelectorAll("#datalist-QuestKeys option");
const form = document.querySelector("form[onsubmit*=\"doAcquireEquipment('QuestKeys')\"]");
const formQuestKeys = document.querySelector("form[onsubmit*=\"doAcquireEquipment('QuestKeys')\"]");
const giveAllQuestButton = document.querySelector("button[onclick*=\"doBulkQuestUpdate('giveAll')\"]");
if (datalistQuestKeys.length === 0) {
form.classList.add("disabled");
form.querySelector("input").disabled = true;
form.querySelector("button").disabled = true;
formQuestKeys.classList.add("disabled");
formQuestKeys.querySelector("input").disabled = true;
formQuestKeys.querySelector("button").disabled = true;
giveAllQuestButton.disabled = true;
} else {
form.classList.remove("disabled");
form.querySelector("input").disabled = false;
form.querySelector("button").disabled = false;
formQuestKeys.classList.remove("disabled");
formQuestKeys.querySelector("input").disabled = false;
formQuestKeys.querySelector("button").disabled = false;
giveAllQuestButton.disabled = false;
}
@ -1079,6 +1173,16 @@ function doAcquireModularEquipment(category, WeaponType) {
}
}
function doAcquireEvolution() {
const uniqueName = getKey(document.getElementById("acquire-type-EvolutionProgress"));
if (!uniqueName) {
$("#acquire-type-EvolutionProgress").addClass("is-invalid").focus();
return;
}
setEvolutionProgress([{ ItemType: uniqueName, Rank: permanentEvolutionWeapons.has(uniqueName) ? 0 : 1 }]);
}
$("input[list]").on("input", function () {
$(this).removeClass("is-invalid");
});
@ -1116,9 +1220,44 @@ function addMissingEquipment(categories) {
}
}
function maxRankAllEquipment(categories) {
const req = $.get("/api/inventory.php?" + window.authz + "&xpBasedLevelCapDisabled=1");
function addMissingEvolutionProgress() {
const requests = [];
document.querySelectorAll("#datalist-EvolutionProgress option").forEach(elm => {
const uniqueName = elm.getAttribute("data-key");
requests.push({ ItemType: uniqueName, Rank: permanentEvolutionWeapons.has(uniqueName) ? 0 : 1 });
});
if (requests.length != 0 && window.confirm(loc("code_addItemsConfirm").split("|COUNT|").join(requests.length))) {
setEvolutionProgress(requests);
}
}
function maxRankAllEvolutions() {
revalidateAuthz(() => {
const req = $.get("/api/inventory.php?" + window.authz + "&xpBasedLevelCapDisabled=1");
req.done(data => {
const requests = [];
data.EvolutionProgress.forEach(item => {
if (item.Rank < 5) {
requests.push({
ItemType: item.ItemType,
Rank: 5
});
}
});
if (Object.keys(requests).length > 0) {
return setEvolutionProgress(requests);
}
toast(loc("code_noEquipmentToRankUp"));
});
});
}
function maxRankAllEquipment(categories) {
revalidateAuthz(() => {
const req = $.get("/api/inventory.php?" + window.authz + "&xpBasedLevelCapDisabled=1");
req.done(data => {
window.itemListPromise.then(itemMap => {
const batchData = {};
@ -1147,7 +1286,8 @@ function maxRankAllEquipment(categories) {
for (const exaltedType of itemMap[item.ItemType].exalted) {
const exaltedItem = data["SpecialItems"].find(x => x.ItemType == exaltedType);
if (exaltedItem) {
const exaltedCap = itemMap[exaltedType]?.type == "weapons" ? 800_000 : 1_600_000;
const exaltedCap =
itemMap[exaltedType]?.type == "weapons" ? 800_000 : 1_600_000;
if (exaltedItem.XP < exaltedCap) {
batchData["SpecialItems"] ??= [];
batchData["SpecialItems"].push({
@ -1169,8 +1309,10 @@ function maxRankAllEquipment(categories) {
toast(loc("code_noEquipmentToRankUp"));
});
});
});
}
// Assumes that caller revalidates authz
function addGearExp(category, oid, xp) {
const data = {};
data[category] = [
@ -1179,7 +1321,6 @@ function addGearExp(category, oid, xp) {
XP: xp
}
];
revalidateAuthz(() => {
$.post({
url: "/custom/addXp?" + window.authz,
contentType: "application/json",
@ -1189,7 +1330,6 @@ function addGearExp(category, oid, xp) {
updateInventory();
}
});
});
}
function sendBatchGearExp(data) {
@ -1303,6 +1443,19 @@ function maturePet(oid, revert) {
});
}
function setEvolutionProgress(requests) {
revalidateAuthz(() => {
const req = $.post({
url: "/custom/setEvolutionProgress?" + window.authz,
contentType: "application/json",
data: JSON.stringify(requests)
});
req.done(() => {
updateInventory();
});
});
}
function doAcquireMiscItems() {
const uniqueName = getKey(document.getElementById("miscitem-type"));
if (!uniqueName) {
@ -1450,6 +1603,7 @@ function doAcquireMod() {
const uiConfigs = [...$("#server-settings input[id]")].map(x => x.id);
function doChangeSettings() {
revalidateAuthz(() => {
fetch("/custom/config?" + window.authz)
.then(response => response.json())
.then(json => {
@ -1476,6 +1630,7 @@ function doChangeSettings() {
updateInventory();
});
});
});
}
// Cheats route
@ -1647,6 +1802,14 @@ function doRemoveUnrankedMods() {
});
}
function doAddMissingMaxRankMods() {
revalidateAuthz(() => {
fetch("/custom/addMissingMaxRankMods?" + window.authz).then(() => {
updateInventory();
});
});
}
// Powersuit Route
single.getRoute("#powersuit-route").on("beforeload", function () {
@ -1720,6 +1883,7 @@ function doChangeSupportedSyndicate() {
}
function doAddCurrency(currency) {
revalidateAuthz(() => {
$.post({
url: "/custom/addCurrency?" + window.authz,
contentType: "application/json",
@ -1730,24 +1894,29 @@ function doAddCurrency(currency) {
}).then(function () {
updateInventory();
});
});
}
function doQuestUpdate(operation, itemType) {
revalidateAuthz(() => {
$.post({
url: "/custom/manageQuests?" + window.authz + "&operation=" + operation + "&itemType=" + itemType,
contentType: "application/json"
}).then(function () {
updateInventory();
});
});
}
function doBulkQuestUpdate(operation) {
revalidateAuthz(() => {
$.post({
url: "/custom/manageQuests?" + window.authz + "&operation=" + operation,
contentType: "application/json"
}).then(function () {
updateInventory();
});
});
}
function toast(text) {

View File

@ -34,6 +34,8 @@ dict = {
code_rerollsNumber: `Anzahl der Umrollversuche`,
code_viewStats: `Statistiken anzeigen`,
code_rank: `Rang`,
code_rankUp: `Rang erhöhen`,
code_rankDown: `Rang verringern`,
code_count: `Anzahl`,
code_focusAllUnlocked: `Alle Fokus-Schulen sind bereits freigeschaltet.`,
code_focusUnlocked: `|COUNT| neue Fokus-Schulen freigeschaltet! Ein Inventar-Update wird benötigt, damit die Änderungen im Spiel sichtbar werden. Die Sternenkarte zu besuchen, sollte der einfachste Weg sein, dies auszulösen.`,
@ -60,7 +62,7 @@ dict = {
login_emailLabel: `E-Mail-Adresse`,
login_passwordLabel: `Passwort`,
login_loginButton: `Anmelden`,
login_registerButton: `[UNTRANSLATED] Register`,
login_registerButton: `Registrieren`,
navbar_logout: `Abmelden`,
navbar_renameAccount: `Account umbenennen`,
navbar_deleteAccount: `Account löschen`,
@ -82,20 +84,23 @@ dict = {
inventory_sentinelWeapons: `Wächter-Waffen`,
inventory_operatorAmps: `Verstärker`,
inventory_hoverboards: `K-Drives`,
inventory_moaPets: `Moa`,
inventory_moaPets: `Moas`,
inventory_kubrowPets: `Bestien`,
inventory_evolutionProgress: `Incarnon-Entwicklungsfortschritte`,
inventory_bulkAddSuits: `Fehlende Warframes hinzufügen`,
inventory_bulkAddWeapons: `Fehlende Waffen hinzufügen`,
inventory_bulkAddSpaceSuits: `Fehlende Archwings hinzufügen`,
inventory_bulkAddSpaceWeapons: `Fehlende Archwing-Waffen hinzufügen`,
inventory_bulkAddSentinels: `Fehlende Wächter hinzufügen`,
inventory_bulkAddSentinelWeapons: `Fehlende Wächter-Waffen hinzufügen`,
inventory_bulkAddEvolutionProgress: `Fehlende Incarnon-Entwicklungsfortschritte hinzufügen`,
inventory_bulkRankUpSuits: `Alle Warframes auf Max. Rang`,
inventory_bulkRankUpWeapons: `Alle Waffen auf Max. Rang`,
inventory_bulkRankUpSpaceSuits: `Alle Archwings auf Max. Rang`,
inventory_bulkRankUpSpaceWeapons: `Alle Archwing-Waffen auf Max. Rang`,
inventory_bulkRankUpSentinels: `Alle Wächter auf Max. Rang`,
inventory_bulkRankUpSentinelWeapons: `Alle Wächter-Waffen auf Max. Rang`,
inventory_bulkRankUpEvolutionProgress: `Alle Incarnon-Entwicklungsfortschritte auf Max. Rang`,
quests_list: `Quests`,
quests_completeAll: `Alle Quests abschließen`,
@ -115,8 +120,9 @@ dict = {
mods_fingerprintHelp: `Benötigst du Hilfe mit dem Fingerabdruck?`,
mods_rivens: `Rivens`,
mods_mods: `Mods`,
mods_bulkAddMods: `Fehlende Mods hinzufügen`,
mods_removeUnranked: `[UNTRANSLATED] Remove Unranked Mods`,
mods_addMissingUnrankedMods: `Fehlende Mods ohne Rang hinzufügen`,
mods_removeUnranked: `Mods ohne Rang entfernen`,
mods_addMissingMaxRankMods: `Fehlende Mods mit Max. Rang hinzufügen`,
cheats_administratorRequirement: `Du musst Administrator sein, um diese Funktion nutzen zu können. Um Administrator zu werden, füge <code>|DISPLAYNAME|</code> zu <code>administratorNames</code> in der config.json hinzu.`,
cheats_server: `Server`,
cheats_skipTutorial: `Tutorial überspringen`,
@ -128,6 +134,7 @@ dict = {
cheats_infiniteEndo: `Unendlich Endo`,
cheats_infiniteRegalAya: `Unendlich Reines Aya`,
cheats_infiniteHelminthMaterials: `Unendlich Helminth-Materialien`,
cheats_dontSubtractConsumables: `Verbrauchsgegenstände (Ausrüstung) nicht verbrauchen`,
cheats_unlockAllShipFeatures: `Alle Schiffs-Funktionen freischalten`,
cheats_unlockAllShipDecorations: `Alle Schiffsdekorationen freischalten`,
cheats_unlockAllFlavourItems: `Alle <abbr title=\"Animationssets, Glyphen, Farbpaletten usw.\">Sammlerstücke</abbr> freischalten`,
@ -147,6 +154,7 @@ dict = {
cheats_noKimCooldowns: `Keine Wartezeit bei KIM`,
cheats_instantResourceExtractorDrones: `Sofortige Ressourcen-Extraktor-Drohnen`,
cheats_noResourceExtractorDronesDamage: `Kein Schaden für Ressourcen-Extraktor-Drohnen`,
cheats_skipClanKeyCrafting: `Clan-Schlüsselherstellung überspringen`,
cheats_noDojoRoomBuildStage: `Kein Dojo-Raum-Bauvorgang`,
cheats_noDojoDecoBuildStage: `Kein Dojo-Deko-Bauvorgang`,
cheats_fastDojoRoomDestruction: `Schnelle Dojo-Raum-Zerstörung`,

View File

@ -33,6 +33,8 @@ dict = {
code_rerollsNumber: `Number of rerolls`,
code_viewStats: `View Stats`,
code_rank: `Rank`,
code_rankUp: `Rank up`,
code_rankDown: `Rank down`,
code_count: `Count`,
code_focusAllUnlocked: `All focus schools are already unlocked.`,
code_focusUnlocked: `Unlocked |COUNT| new focus schools! An inventory update will be needed for the changes to be reflected in-game. Visiting the navigation should be the easiest way to trigger that.`,
@ -81,20 +83,23 @@ dict = {
inventory_sentinelWeapons: `Sentinel Weapons`,
inventory_operatorAmps: `Amps`,
inventory_hoverboards: `K-Drives`,
inventory_moaPets: `Moa`,
inventory_moaPets: `Moas`,
inventory_kubrowPets: `Beasts`,
inventory_evolutionProgress: `Incarnon Evolution Progress`,
inventory_bulkAddSuits: `Add Missing Warframes`,
inventory_bulkAddWeapons: `Add Missing Weapons`,
inventory_bulkAddSpaceSuits: `Add Missing Archwings`,
inventory_bulkAddSpaceWeapons: `Add Missing Archwing Weapons`,
inventory_bulkAddSentinels: `Add Missing Sentinels`,
inventory_bulkAddSentinelWeapons: `Add Missing Sentinel Weapons`,
inventory_bulkAddEvolutionProgress: `Add Missing Incarnon Evolution Progress`,
inventory_bulkRankUpSuits: `Max Rank All Warframes`,
inventory_bulkRankUpWeapons: `Max Rank All Weapons`,
inventory_bulkRankUpSpaceSuits: `Max Rank All Archwings`,
inventory_bulkRankUpSpaceWeapons: `Max Rank All Archwing Weapons`,
inventory_bulkRankUpSentinels: `Max Rank All Sentinels`,
inventory_bulkRankUpSentinelWeapons: `Max Rank All Sentinel Weapons`,
inventory_bulkRankUpEvolutionProgress: `Max Rank All Incarnon Evolution Progress`,
quests_list: `Quests`,
quests_completeAll: `Complete All Quests`,
@ -114,8 +119,9 @@ dict = {
mods_fingerprintHelp: `Need help with the fingerprint?`,
mods_rivens: `Rivens`,
mods_mods: `Mods`,
mods_bulkAddMods: `Add Missing Mods`,
mods_addMissingUnrankedMods: `Add Missing Unranked Mods`,
mods_removeUnranked: `Remove Unranked Mods`,
mods_addMissingMaxRankMods: `Add Missing Max Rank Mods`,
cheats_administratorRequirement: `You must be an administrator to use this feature. To become an administrator, add <code>|DISPLAYNAME|</code> to <code>administratorNames</code> in the config.json.`,
cheats_server: `Server`,
cheats_skipTutorial: `Skip Tutorial`,
@ -127,6 +133,7 @@ dict = {
cheats_infiniteEndo: `Infinite Endo`,
cheats_infiniteRegalAya: `Infinite Regal Aya`,
cheats_infiniteHelminthMaterials: `Infinite Helminth Materials`,
cheats_dontSubtractConsumables: `Don't Subtract Consumables`,
cheats_unlockAllShipFeatures: `Unlock All Ship Features`,
cheats_unlockAllShipDecorations: `Unlock All Ship Decorations`,
cheats_unlockAllFlavourItems: `Unlock All <abbr title=\"Animation Sets, Glyphs, Palettes, etc.\">Flavor Items</abbr>`,
@ -146,6 +153,7 @@ dict = {
cheats_noKimCooldowns: `No KIM Cooldowns`,
cheats_instantResourceExtractorDrones: `Instant Resource Extractor Drones`,
cheats_noResourceExtractorDronesDamage: `No Resource Extractor Drones Damage`,
cheats_skipClanKeyCrafting: `Skip Clan Key Crafting`,
cheats_noDojoRoomBuildStage: `No Dojo Room Build Stage`,
cheats_noDojoDecoBuildStage: `No Dojo Deco Build Stage`,
cheats_fastDojoRoomDestruction: `Fast Dojo Room Destruction`,

View File

@ -34,6 +34,8 @@ dict = {
code_rerollsNumber: `Cantidad de reintentos`,
code_viewStats: `Ver estadísticas`,
code_rank: `Rango`,
code_rankUp: `Subir de rango`,
code_rankDown: `Bajar de rango`,
code_count: `Cantidad`,
code_focusAllUnlocked: `Todas las escuelas de enfoque ya están desbloqueadas.`,
code_focusUnlocked: `¡Desbloqueadas |COUNT| nuevas escuelas de enfoque! Se necesita una actualización del inventario para reflejar los cambios en el juego. Visitar la navegación debería ser la forma más sencilla de activarlo.`,
@ -60,7 +62,7 @@ dict = {
login_emailLabel: `Dirección de correo electrónico`,
login_passwordLabel: `Contraseña`,
login_loginButton: `Iniciar sesión`,
login_registerButton: `[UNTRANSLATED] Register`,
login_registerButton: `Registrarse`,
navbar_logout: `Cerrar sesión`,
navbar_renameAccount: `Renombrar cuenta`,
navbar_deleteAccount: `Eliminar cuenta`,
@ -82,20 +84,23 @@ dict = {
inventory_sentinelWeapons: `Armas de centinela`,
inventory_operatorAmps: `Amps`,
inventory_hoverboards: `K-Drives`,
inventory_moaPets: `Moa`,
inventory_moaPets: `Moas`,
inventory_kubrowPets: `Bestias`,
inventory_evolutionProgress: `Progreso de evolución Incarnon`,
inventory_bulkAddSuits: `Agregar Warframes faltantes`,
inventory_bulkAddWeapons: `Agregar armas faltantes`,
inventory_bulkAddSpaceSuits: `Agregar Archwings faltantes`,
inventory_bulkAddSpaceWeapons: `Agregar armas Archwing faltantes`,
inventory_bulkAddSentinels: `Agregar centinelas faltantes`,
inventory_bulkAddSentinelWeapons: `Agregar armas de centinela faltantes`,
inventory_bulkAddEvolutionProgress: `Completar el progreso de evolución Incarnon faltante`,
inventory_bulkRankUpSuits: `Maximizar rango de todos los Warframes`,
inventory_bulkRankUpWeapons: `Maximizar rango de todas las armas`,
inventory_bulkRankUpSpaceSuits: `Maximizar rango de todos los Archwings`,
inventory_bulkRankUpSpaceWeapons: `Maximizar rango de todas las armas Archwing`,
inventory_bulkRankUpSentinels: `Maximizar rango de todos los centinelas`,
inventory_bulkRankUpSentinelWeapons: `Maximizar rango de todas las armas de centinela`,
inventory_bulkRankUpEvolutionProgress: `Maximizar todo el progreso de evolución Incarnon`,
quests_list: `Misiones`,
quests_completeAll: `Completar todas las misiones`,
@ -115,8 +120,9 @@ dict = {
mods_fingerprintHelp: `¿Necesitas ayuda con la huella digital?`,
mods_rivens: `Agrietados`,
mods_mods: `Mods`,
mods_bulkAddMods: `Agregar mods faltantes`,
mods_removeUnranked: `[UNTRANSLATED] Remove Unranked Mods`,
mods_addMissingUnrankedMods: `Agregar mods sin rango faltantes`,
mods_removeUnranked: `Quitar mods sin rango`,
mods_addMissingMaxRankMods: `Agregar mods de rango máximo faltantes`,
cheats_administratorRequirement: `Debes ser administrador para usar esta función. Para convertirte en administrador, agrega <code>|DISPLAYNAME|</code> a <code>administratorNames</code> en el archivo config.json.`,
cheats_server: `Servidor`,
cheats_skipTutorial: `Omitir tutorial`,
@ -128,6 +134,7 @@ dict = {
cheats_infiniteEndo: `Endo infinito`,
cheats_infiniteRegalAya: `Aya Real infinita`,
cheats_infiniteHelminthMaterials: `Materiales Helminto infinitos`,
cheats_dontSubtractConsumables: `No restar consumibles`,
cheats_unlockAllShipFeatures: `Desbloquear todas las funciones de nave`,
cheats_unlockAllShipDecorations: `Desbloquear todas las decoraciones de nave`,
cheats_unlockAllFlavourItems: `Desbloquear todos los <abbr title="Conjuntos de animaciones, glifos, paletas, etc.">ítems estéticos</abbr>`,
@ -147,6 +154,7 @@ dict = {
cheats_noKimCooldowns: `Sin tiempo de espera para conversaciones KIM`,
cheats_instantResourceExtractorDrones: `Drones de extracción de recursos instantáneos`,
cheats_noResourceExtractorDronesDamage: `Sin daño a los drones extractores de recursos`,
cheats_skipClanKeyCrafting: `Saltar la fabricación de la llave de clan`,
cheats_noDojoRoomBuildStage: `Sin etapa de construcción de sala del dojo`,
cheats_noDojoDecoBuildStage: `Sin etapa de construcción de decoraciones del dojo`,
cheats_fastDojoRoomDestruction: `Destrucción rápida de salas del dojo`,

View File

@ -34,6 +34,8 @@ dict = {
code_rerollsNumber: `Nombre de rerolls`,
code_viewStats: `Voir les stats`,
code_rank: `Rang`,
code_rankUp: `[UNTRANSLATED] Rank up`,
code_rankDown: `[UNTRANSLATED] Rank down`,
code_count: `Quantité`,
code_focusAllUnlocked: `Les écoles de Focus sont déjà déverrouillées.`,
code_focusUnlocked: `|COUNT| écoles de Focus déverrouillées ! Synchronisation de l'inventaire nécessaire.`,
@ -82,20 +84,23 @@ dict = {
inventory_sentinelWeapons: `Armes de sentinelles`,
inventory_operatorAmps: `Amplificateurs`,
inventory_hoverboards: `K-Drives`,
inventory_moaPets: `Moa`,
inventory_moaPets: `Moas`,
inventory_kubrowPets: `Bêtes`,
inventory_evolutionProgress: `[UNTRANSLATED] Incarnon Evolution Progress`,
inventory_bulkAddSuits: `Ajouter les Warframes manquantes`,
inventory_bulkAddWeapons: `Ajouter les armes manquantes`,
inventory_bulkAddSpaceSuits: `Ajouter les Archwings manquants`,
inventory_bulkAddSpaceWeapons: `Ajouter les armes d'Archwing manquantes`,
inventory_bulkAddSentinels: `Ajouter les Sentinelles manquantes`,
inventory_bulkAddSentinelWeapons: `Ajouter les armes de Sentinelles manquantes`,
inventory_bulkAddEvolutionProgress: `[UNTRANSLATED] Add Missing Incarnon Evolution Progress`,
inventory_bulkRankUpSuits: `Toutes les Warframes rang max`,
inventory_bulkRankUpWeapons: `Toutes les armes rang max`,
inventory_bulkRankUpSpaceSuits: `Tous les Archwings rang max`,
inventory_bulkRankUpSpaceWeapons: `Toutes les armes d'Archwing rang max`,
inventory_bulkRankUpSentinels: `Toutes les Sentinelles rang max`,
inventory_bulkRankUpSentinelWeapons: `Toutes les armes de Sentinelles rang max`,
inventory_bulkRankUpEvolutionProgress: `[UNTRANSLATED] Max Rank All Incarnon Evolution Progress`,
quests_list: `Quêtes`,
quests_completeAll: `Compléter toutes les quêtes`,
@ -115,8 +120,9 @@ dict = {
mods_fingerprintHelp: `Besoin d'aide pour l'empreinte ?`,
mods_rivens: `Rivens`,
mods_mods: `Mods`,
mods_bulkAddMods: `Ajouter les mods manquants`,
mods_addMissingUnrankedMods: `[UNTRANSLATED] Add Missing Unranked Mods`,
mods_removeUnranked: `[UNTRANSLATED] Remove Unranked Mods`,
mods_addMissingMaxRankMods: `[UNTRANSLATED] Add Missing Max Rank Mods`,
cheats_administratorRequirement: `Rôle d'administrateur requis pour cette fonctionnalité. Ajoutez <code>|DISPLAYNAME|</code> à la ligne <code>administratorNames</code> dans le fichier config.json.`,
cheats_server: `Serveur`,
cheats_skipTutorial: `Passer le tutoriel`,
@ -128,6 +134,7 @@ dict = {
cheats_infiniteEndo: `Endo infini`,
cheats_infiniteRegalAya: `Aya Raffiné infini`,
cheats_infiniteHelminthMaterials: `Ressources d'Helminth infinies`,
cheats_dontSubtractConsumables: `[UNTRANSLATED] Don't Subtract Consumables`,
cheats_unlockAllShipFeatures: `Débloquer tous les segments du vaisseau`,
cheats_unlockAllShipDecorations: `Débloquer toutes les décorations du vaisseau`,
cheats_unlockAllFlavourItems: `Débloquer tous les <abbr title=\"Animations, Glyphes, Palettes, etc.\">Flavor Items</abbr>`,
@ -147,6 +154,7 @@ dict = {
cheats_noKimCooldowns: `Aucun cooldown sur le KIM`,
cheats_instantResourceExtractorDrones: `Ressources de drones d'extraction instantannées`,
cheats_noResourceExtractorDronesDamage: `Aucun dégâts aux drones d'extraction de resources`,
cheats_skipClanKeyCrafting: `[UNTRANSLATED] Skip Clan Key Crafting`,
cheats_noDojoRoomBuildStage: `Aucune attente (construction des salles)`,
cheats_noDojoDecoBuildStage: `Aucune attente (construction des décorations)`,
cheats_fastDojoRoomDestruction: `Destruction de salle instantanée (Dojo)`,

View File

@ -34,6 +34,8 @@ dict = {
code_rerollsNumber: `Количество циклов`,
code_viewStats: `Просмотр характеристики`,
code_rank: `Ранг`,
code_rankUp: `Повысить Ранг`,
code_rankDown: `Понизить Ранг`,
code_count: `Количество`,
code_focusAllUnlocked: `Все школы фокуса уже разблокированы.`,
code_focusUnlocked: `Разблокировано |COUNT| новых школ фокуса! Для отображения изменений в игре потребуется обновление инвентаря. Посещение навигации — самый простой способ этого добиться.`,
@ -84,18 +86,21 @@ dict = {
inventory_hoverboards: `К-Драйвы`,
inventory_moaPets: `МОА`,
inventory_kubrowPets: `Звери`,
inventory_evolutionProgress: `Прогресс эволюции Инкарнонов`,
inventory_bulkAddSuits: `Добавить отсутствующие варфреймы`,
inventory_bulkAddWeapons: `Добавить отсутствующее оружие`,
inventory_bulkAddSpaceSuits: `Добавить отсутствующие арчвинги`,
inventory_bulkAddSpaceWeapons: `Добавить отсутствующее оружие арчвингов`,
inventory_bulkAddSentinels: `Добавить отсутствующих стражей`,
inventory_bulkAddSentinelWeapons: `Добавить отсутствующее оружие стражей`,
inventory_bulkAddEvolutionProgress: `Добавить отсуствующий прогресс эволюции Инкарнонов`,
inventory_bulkRankUpSuits: `Максимальный ранг всех варфреймов`,
inventory_bulkRankUpWeapons: `Максимальный ранг всего оружия`,
inventory_bulkRankUpSpaceSuits: `Максимальный ранг всех арчвингов`,
inventory_bulkRankUpSpaceWeapons: `Максимальный ранг всего оружия арчвингов`,
inventory_bulkRankUpSentinels: `Максимальный ранг всех стражей`,
inventory_bulkRankUpSentinelWeapons: `Максимальный ранг всего оружия стражей`,
inventory_bulkRankUpEvolutionProgress: `Максимальный ранг всех эволюций Инкарнонов`,
quests_list: `Квесты`,
quests_completeAll: `Завершить все квесты`,
@ -115,8 +120,9 @@ dict = {
mods_fingerprintHelp: `Нужна помощь с отпечатком?`,
mods_rivens: `Моды Разлома`,
mods_mods: `Моды`,
mods_bulkAddMods: `Добавить отсутствующие моды`,
mods_addMissingUnrankedMods: `[UNTRANSLATED] Add Missing Unranked Mods`,
mods_removeUnranked: `[UNTRANSLATED] Remove Unranked Mods`,
mods_addMissingMaxRankMods: `[UNTRANSLATED] Add Missing Max Rank Mods`,
cheats_administratorRequirement: `Вы должны быть администратором для использования этой функции. Чтобы стать администратором, добавьте <code>\"|DISPLAYNAME|\"</code> в <code>administratorNames</code> в config.json.`,
cheats_server: `Сервер`,
cheats_skipTutorial: `Пропустить обучение`,
@ -128,6 +134,7 @@ dict = {
cheats_infiniteEndo: `Бесконечное эндо`,
cheats_infiniteRegalAya: `Бесконечная Королевская Айя`,
cheats_infiniteHelminthMaterials: `Бесконечные Выделения Гельминта`,
cheats_dontSubtractConsumables: `[UNTRANSLATED] Don't Subtract Consumables`,
cheats_unlockAllShipFeatures: `Разблокировать все функции корабля`,
cheats_unlockAllShipDecorations: `Разблокировать все украшения корабля`,
cheats_unlockAllFlavourItems: `Разблокировать все <abbr title=\"Наборы анимаций, глифы, палитры и т. д.\">уникальные предметы</abbr>`,
@ -147,6 +154,7 @@ dict = {
cheats_noKimCooldowns: `Чаты KIM без кулдауна`,
cheats_instantResourceExtractorDrones: `Мгновенные Экстракторы Ресурсов`,
cheats_noResourceExtractorDronesDamage: `Без урона по дронам-сборщикам`,
cheats_skipClanKeyCrafting: `[UNTRANSLATED] Skip Clan Key Crafting`,
cheats_noDojoRoomBuildStage: `Мгновенное Строительтво Комнат Додзё`,
cheats_noDojoDecoBuildStage: `Мгновенное Строительтво Декораций Додзё`,
cheats_fastDojoRoomDestruction: `Мгновенные Уничтожение Комнат Додзё`,

View File

@ -34,6 +34,8 @@ dict = {
code_rerollsNumber: `洗卡次数`,
code_viewStats: `查看属性`,
code_rank: `等级`,
code_rankUp: `[UNTRANSLATED] Rank up`,
code_rankDown: `[UNTRANSLATED] Rank down`,
code_count: `数量`,
code_focusAllUnlocked: `所有专精学派均已解锁。`,
code_focusUnlocked: `已解锁 |COUNT| 个新专精学派!需要游戏内仓库更新才能生效,您可以通过访问星图来触发仓库更新。`,
@ -84,18 +86,21 @@ dict = {
inventory_hoverboards: `K式悬浮板`,
inventory_moaPets: `恐鸟`,
inventory_kubrowPets: `[UNTRANSLATED] Beasts`,
inventory_evolutionProgress: `[UNTRANSLATED] Incarnon Evolution Progress`,
inventory_bulkAddSuits: `添加缺失战甲`,
inventory_bulkAddWeapons: `添加缺失武器`,
inventory_bulkAddSpaceSuits: `添加缺失Archwing`,
inventory_bulkAddSpaceWeapons: `添加缺失Archwing武器`,
inventory_bulkAddSentinels: `添加缺失守护`,
inventory_bulkAddSentinelWeapons: `添加缺失守护武器`,
inventory_bulkAddEvolutionProgress: `[UNTRANSLATED] Add Missing Incarnon Evolution Progress`,
inventory_bulkRankUpSuits: `所有战甲升满级`,
inventory_bulkRankUpWeapons: `所有武器升满级`,
inventory_bulkRankUpSpaceSuits: `所有Archwing升满级`,
inventory_bulkRankUpSpaceWeapons: `所有Archwing武器升满级`,
inventory_bulkRankUpSentinels: `所有守护升满级`,
inventory_bulkRankUpSentinelWeapons: `所有守护武器升满级`,
inventory_bulkRankUpEvolutionProgress: `[UNTRANSLATED] Max Rank All Incarnon Evolution Progress`,
quests_list: `任务`,
quests_completeAll: `完成所有任务`,
@ -115,8 +120,9 @@ dict = {
mods_fingerprintHelp: `需要印记相关的帮助?`,
mods_rivens: `裂罅MOD`,
mods_mods: `Mods`,
mods_bulkAddMods: `添加缺失MOD`,
mods_addMissingUnrankedMods: `[UNTRANSLATED] Add Missing Unranked Mods`,
mods_removeUnranked: `[UNTRANSLATED] Remove Unranked Mods`,
mods_addMissingMaxRankMods: `[UNTRANSLATED] Add Missing Max Rank Mods`,
cheats_administratorRequirement: `您必须是管理员才能使用此功能。要成为管理员,请将 <code>|DISPLAYNAME|</code> 添加到 config.json 的 <code>administratorNames</code> 中。`,
cheats_server: `服务器`,
cheats_skipTutorial: `跳过教程`,
@ -128,6 +134,7 @@ dict = {
cheats_infiniteEndo: `无限内融核心`,
cheats_infiniteRegalAya: `无限御品阿耶`,
cheats_infiniteHelminthMaterials: `无限Helminth材料`,
cheats_dontSubtractConsumables: `[UNTRANSLATED] Don't Subtract Consumables`,
cheats_unlockAllShipFeatures: `解锁所有飞船功能`,
cheats_unlockAllShipDecorations: `解锁所有飞船装饰`,
cheats_unlockAllFlavourItems: `解锁所有<abbr title=\"动画组合、图标、调色板等\">装饰物品</abbr>`,
@ -147,6 +154,7 @@ dict = {
cheats_noKimCooldowns: `[UNTRANSLATED] No KIM Cooldowns`,
cheats_instantResourceExtractorDrones: `即时资源采集无人机`,
cheats_noResourceExtractorDronesDamage: `[UNTRANSLATED] No Resource Extractor Drones Damage`,
cheats_skipClanKeyCrafting: `[UNTRANSLATED] Skip Clan Key Crafting`,
cheats_noDojoRoomBuildStage: `无视道场房间建造阶段`,
cheats_noDojoDecoBuildStage: `[UNTRANSLATED] No Dojo Deco Build Stage`,
cheats_fastDojoRoomDestruction: `快速拆除道场房间`,