Compare commits

...

25 Commits

Author SHA1 Message Date
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
37 changed files with 1265 additions and 612 deletions

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

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

@ -3,7 +3,7 @@ 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";
@ -63,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({
@ -81,6 +82,7 @@ export const missionInventoryUpdateController: RequestHandler = async (req, res)
ConquestCompletedMissionsCount
} = await addMissionRewards(inventory, missionReport, firstCompletion);
inventory.RewardSeed = generateRewardSeed();
await inventory.save();
const inventoryResponse = await getInventoryResponse(inventory, true);

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 {
@ -138,6 +138,18 @@ 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);
combineInventoryChanges(

View File

@ -45,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) {
@ -148,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 });
@ -281,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

@ -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;
@ -51,6 +52,7 @@ interface ItemLists {
OperatorAmps: ListedItem[];
QuestKeys: ListedItem[];
KubrowPets: ListedItem[];
EvolutionProgress: ListedItem[];
mods: ListedItem[];
}
@ -82,6 +84,7 @@ const getItemListsController: RequestHandler = (req, response) => {
OperatorAmps: [],
QuestKeys: [],
KubrowPets: [],
EvolutionProgress: [],
mods: []
};
for (const [uniqueName, item] of Object.entries(ExportWarframes)) {
@ -283,6 +286,12 @@ const getItemListsController: RequestHandler = (req, response) => {
});
}
}
for (const uniqueName of allIncarnons) {
res.EvolutionProgress.push({
uniqueName,
name: getString(getItemName(uniqueName) || "", lang)
});
}
response.json(res);
};

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

@ -390,7 +390,7 @@ MailboxSchema.set("toJSON", {
const DuviriInfoSchema = new Schema<IDuviriInfo>(
{
Seed: Number,
Seed: BigInt,
NumCompletions: { type: Number, default: 0 }
},
{
@ -1125,15 +1125,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 }

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";
@ -158,6 +159,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);

View File

@ -17,10 +17,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();
@ -42,6 +43,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

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

View File

@ -169,6 +169,7 @@ const convertPendingRecipe = (client: IPendingRecipeClient): IPendingRecipeDatab
const convertNemesis = (client: INemesisClient): INemesisDatabase => {
return {
...client,
fp: BigInt(client.fp),
d: convertDate(client.d)
};
};

View File

@ -18,7 +18,6 @@ import {
IKubrowPetEggDatabase,
IKubrowPetEggClient,
ILibraryDailyTaskInfo,
ICalendarProgress,
IDroneClient,
IUpgradeClient,
TPartialStartingGear,
@ -26,7 +25,8 @@ import {
ICrewMemberClient,
Status,
IKubrowPetDetailsDatabase,
ITraits
ITraits,
ICalendarProgress
} from "@/src/types/inventoryTypes/inventoryTypes";
import { IGenericUpdate, IUpdateNodeIntrosResponse } from "../types/genericUpdate";
import { IKeyChainRequest, IMissionInventoryUpdateRequest } from "../types/requestTypes";
@ -78,6 +78,7 @@ 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";
export const createInventory = async (
accountOwnerId: Types.ObjectId,
@ -91,7 +92,6 @@ export const createInventory = async (
});
inventory.LibraryAvailableDailyTaskInfo = createLibraryDailyTask();
inventory.CalendarProgress = createCalendar();
inventory.RewardSeed = generateRewardSeed();
inventory.DuviriInfo = {
Seed: generateRewardSeed(),
@ -120,10 +120,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 +143,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" }],
@ -240,7 +245,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);
@ -851,7 +856,7 @@ export const addPowerSuit = async (
inventory: TInventoryDatabaseDocument,
powersuitName: string,
inventoryChanges: IInventoryChanges = {},
features: number | undefined = undefined
features?: number
): Promise<IInventoryChanges> => {
const powersuit = ExportWarframes[powersuitName] as IPowersuit | undefined;
const exalted = powersuit?.exalted ?? [];
@ -883,7 +888,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 ?? [];
@ -935,7 +940,7 @@ export const addSpaceSuit = (
inventory: TInventoryDatabaseDocument,
spacesuitName: string,
inventoryChanges: IInventoryChanges = {},
features: number | undefined = undefined
features?: number
): IInventoryChanges => {
const suitIndex =
inventory.SpaceSuits.push({
@ -1194,9 +1199,9 @@ export const addEquipment = (
inventory: TInventoryDatabaseDocument,
category: TEquipmentKey,
type: string,
modularParts: string[] | undefined = undefined,
modularParts?: string[],
inventoryChanges: IInventoryChanges = {},
defaultOverwrites: Partial<IEquipmentDatabase> | undefined = undefined
defaultOverwrites?: Partial<IEquipmentDatabase>
): IInventoryChanges => {
const equipment = Object.assign(
{
@ -1236,12 +1241,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;
};
@ -1752,20 +1761,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,
@ -1802,3 +1797,37 @@ export const cleanupInventory = (inventory: TInventoryDatabaseDocument): void =>
LibrarySyndicate.FreeFavorsEarned = undefined;
}
};
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;
};

View File

@ -33,6 +33,7 @@ import {
addStanding,
combineInventoryChanges,
generateRewardSeed,
getCalendarProgress,
updateCurrency,
updateSyndicate
} from "@/src/services/inventoryService";
@ -73,13 +74,11 @@ const getRotations = (rewardInfo: IRewardInfo, tierOverride?: number): number[]
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:
// - Aborting a railjack mission (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1741)
// - Completing only 1 zone of (E)SO (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1823)
const rotationPattern =
tierOverride === undefined
@ -562,6 +561,22 @@ 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;
}
default:
// Equipment XP updates
if (equipmentKeys.includes(key as TEquipmentKey)) {
@ -742,11 +757,6 @@ 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);
@ -792,7 +802,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;
@ -804,6 +815,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, [
{
@ -1406,6 +1425,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

@ -97,9 +97,17 @@ 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 {

View File

@ -1,13 +1,8 @@
import { unixTimesInMs } from "@/src/constants/timeConstants";
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";
@ -23,7 +18,6 @@ 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";
@ -32,14 +26,14 @@ import OstronFishmongerVendorManifest from "@/static/fixed_responses/getVendorIn
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,
@ -54,7 +48,6 @@ const rawVendorManifests: IRawVendorManifest[] = [
DuviriAcrithisVendorManifest,
EntratiLabsEntratiLabsCommisionsManifest,
EntratiLabsEntratiLabVendorManifest,
GuildAdvertisementVendorManifest, // uses preprocessing
HubsIronwakeDondaVendorManifest, // uses preprocessing
HubsRailjackCrewMemberVendorManifest,
MaskSalesmanManifest,
@ -63,16 +56,16 @@ const rawVendorManifests: IRawVendorManifest[] = [
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;
}
@ -84,7 +77,7 @@ const generatableVendors: IGeneratableVendorInfo[] = [
RandomSeedType: "VRST_WEAPON",
RequiredGoalTag: "",
WeaponUpgradeValueAttenuationExponent: 2.25,
cycleStart: 1740960000_000,
cycleOffset: 1740960000_000,
cycleDuration: 4 * unixTimesInMs.day
},
{
@ -93,8 +86,22 @@ const generatableVendors: IGeneratableVendorInfo[] = [
PropertyTextHash: "34F8CF1DFF745F0D67433A5EF0A03E70",
RandomSeedType: "VRST_WEAPON",
WeaponUpgradeValueAttenuationExponent: 2.25,
cycleStart: 1744934400_000,
cycleOffset: 1744934400_000,
cycleDuration: 4 * unixTimesInMs.day
},
{
_id: { $oid: "5be4a159b144f3cdf1c22efa" },
TypeName: "/Lotus/Types/Game/VendorManifests/Solaris/DebtTokenVendorManifest",
PropertyTextHash: "A39621049CA3CA13761028CD21C239EF",
RandomSeedType: "VRST_FLAVOUR_TEXT",
cycleDuration: unixTimesInMs.hour
},
{
_id: { $oid: "61ba123467e5d37975aeeb03" },
TypeName: "/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest",
PropertyTextHash: "255AFE2169BAE4130B4B20D7C55D14FA",
RandomSeedType: "VRST_FLAVOUR_TEXT",
cycleDuration: unixTimesInMs.week
}
// {
// _id: { $oid: "5dbb4c41e966f7886c3ce939" },
@ -103,7 +110,7 @@ const generatableVendors: IGeneratableVendorInfo[] = [
// }
];
export const getVendorManifestByTypeName = (typeName: string): IVendorManifestPreprocessed | undefined => {
export const getVendorManifestByTypeName = (typeName: string): IVendorManifest | undefined => {
for (const vendorManifest of rawVendorManifests) {
if (vendorManifest.VendorInfo.TypeName == typeName) {
return preprocessVendorManifest(vendorManifest);
@ -117,7 +124,7 @@ export const getVendorManifestByTypeName = (typeName: string): IVendorManifestPr
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);
@ -131,29 +138,20 @@ export const getVendorManifestByOid = (oid: string): IVendorManifestPreprocessed
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 +159,136 @@ 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.numItems.minValue != manifest.numItems.maxValue) {
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 (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);
}
}
// 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

@ -7,6 +7,7 @@ import { CRng } from "@/src/services/rngService";
import { eMissionType, ExportNightwave, ExportRegions, IRegion } from "warframe-public-export-plus";
import {
ICalendarDay,
ICalendarEvent,
ICalendarSeason,
ILiteSortie,
ISeasonChallenge,
@ -186,10 +187,12 @@ const pushSyndicateMissions = (
for (const [key, value] of Object.entries(ExportRegions)) {
if (
!isArchwingMission(value) &&
value.systemIndex != 23 && // no 1999 stuff
!value.questReq && // Exclude zariman, murmor, and 1999 stuff
!value.hidden && // Exclude the index
!value.darkSectorData && // Exclude dark sectors
value.missionIndex != 10 && // Exclude MT_PVP (for relays)
value.missionIndex != 23 && // no junctions
value.missionIndex <= 28 // no railjack or some such
value.missionIndex < 28 // no open worlds, railjack, etc
) {
nodeOptions.push(key);
}
@ -206,7 +209,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,
@ -225,7 +228,8 @@ 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);
@ -350,11 +354,11 @@ const pushSortieIfRelevant = (worldState: IWorldState, day: number): void => {
}
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
});
@ -374,7 +378,7 @@ 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,
@ -394,7 +398,7 @@ 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() } },
@ -411,7 +415,7 @@ 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() } },
@ -463,8 +467,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) {
@ -476,7 +480,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",
@ -523,8 +527,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}`);
@ -571,69 +579,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);
@ -644,7 +683,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"]
@ -710,7 +749,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);
@ -719,7 +758,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",
@ -727,7 +766,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",
@ -735,7 +774,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",
@ -749,14 +788,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: [
{
@ -820,13 +863,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: [
{
@ -890,13 +935,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: [
{
@ -1119,7 +1166,8 @@ export const getLiteSortie = (week: number): ILiteSortie => {
}
}
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);
@ -1128,12 +1176,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: [
{

View File

@ -11,6 +11,7 @@ export interface IGuildClient {
Members: IGuildMemberClient[];
Ranks: IGuildRank[];
Tier: number;
Emblem?: boolean;
Vault: IGuildVault;
ActiveDojoColorResearch: string;
Class: number;

View File

@ -134,7 +134,7 @@ export const equipmentKeys = [
export type TEquipmentKey = (typeof equipmentKeys)[number];
export interface IDuviriInfo {
Seed: number;
Seed: bigint;
NumCompletions: number;
}
@ -202,7 +202,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
Mailbox?: IMailboxClient;
SubscribedToEmails: number;
Created: IMongoDate;
RewardSeed: number | bigint;
RewardSeed: bigint;
RegularCredits: number;
PremiumCredits: number;
PremiumCreditsFree: number;
@ -353,7 +353,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 +845,7 @@ export interface IMission extends IMissionDatabase {
}
export interface INemesisBaseClient {
fp: bigint;
fp: bigint | number;
manifest: string;
KillingSuit: string;
killingDamageType: number;
@ -863,7 +863,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 +878,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;
}
@ -1193,17 +1195,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

@ -44,6 +44,7 @@ export type IMissionInventoryUpdateRequest = {
SyndicateId?: string;
SortieId?: string;
CalendarProgress?: { challenge: string }[];
SeasonChallengeCompletions?: ISeasonChallenge[];
AffiliationChanges?: IAffiliationChange[];
crossPlaySetting?: string;
@ -126,6 +127,15 @@ export type IMissionInventoryUpdateRequest = {
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[];
};
@ -135,7 +145,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;

View File

@ -1,18 +1,15 @@
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[];
Bin: string;
QuantityMultiplier: number;
Expiry: IMongoDate; // Either a date in the distant future or a period in milliseconds for preprocessing.
@ -23,10 +20,6 @@ export interface IItemManifest {
Id: IOid;
}
export interface IItemManifestPreprocessed extends Omit<IItemManifest, "ItemPrices"> {
ItemPrices?: IItemPricePreprocessed[];
}
export interface IVendorInfo {
_id: IOid;
TypeName: string;
@ -38,14 +31,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

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

@ -1098,5 +1098,6 @@
"/Lotus/Types/LevelObjects/InfestedPumpkinCocoonLampLarge",
"/Lotus/Types/LevelObjects/InfestedPumpkinCocoonLampSmall",
"/Lotus/Types/LevelObjects/InfestedPumpkinExplosiveTotem",
"/Lotus/Types/Enemies/Orokin/OrokinMoaBipedAvatar"
"/Lotus/Types/Enemies/Orokin/OrokinMoaBipedAvatar",
"/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/BeastMasterAvatar"
]

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

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

@ -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>
@ -733,6 +751,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

@ -78,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
});
});
}
}
@ -183,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 => {
@ -563,6 +577,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 => {
@ -674,18 +764,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;
}
@ -1080,6 +1170,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");
});
@ -1117,6 +1217,40 @@ function addMissingEquipment(categories) {
}
}
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() {
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) {
const req = $.get("/api/inventory.php?" + window.authz + "&xpBasedLevelCapDisabled=1");
@ -1304,6 +1438,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) {

View File

@ -34,6 +34,8 @@ dict = {
code_rerollsNumber: `Anzahl der Umrollversuche`,
code_viewStats: `Statistiken anzeigen`,
code_rank: `Rang`,
code_rankUp: `[UNTRANSLATED] Rank up`,
code_rankDown: `[UNTRANSLATED] Rank down`,
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.`,
@ -84,18 +86,21 @@ dict = {
inventory_hoverboards: `K-Drives`,
inventory_moaPets: `Moa`,
inventory_kubrowPets: `Bestien`,
inventory_evolutionProgress: `[UNTRANSLATED] Incarnon Evolution Progress`,
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: `[UNTRANSLATED] Add Missing Incarnon Evolution Progress`,
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: `[UNTRANSLATED] Max Rank All Incarnon Evolution Progress`,
quests_list: `Quests`,
quests_completeAll: `Alle Quests abschließen`,

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.`,
@ -83,18 +85,21 @@ dict = {
inventory_hoverboards: `K-Drives`,
inventory_moaPets: `Moa`,
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`,

View File

@ -34,6 +34,8 @@ dict = {
code_rerollsNumber: `Cantidad de reintentos`,
code_viewStats: `Ver estadísticas`,
code_rank: `Rango`,
code_rankUp: `[UNTRANSLATED] Rank up`,
code_rankDown: `[UNTRANSLATED] Rank down`,
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.`,
@ -84,18 +86,21 @@ dict = {
inventory_hoverboards: `K-Drives`,
inventory_moaPets: `Moa`,
inventory_kubrowPets: `Bestias`,
inventory_evolutionProgress: `[UNTRANSLATED] Incarnon Evolution Progress`,
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: `[UNTRANSLATED] Add Missing Incarnon Evolution Progress`,
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: `[UNTRANSLATED] Max Rank All Incarnon Evolution Progress`,
quests_list: `Misiones`,
quests_completeAll: `Completar todas las misiones`,

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.`,
@ -84,18 +86,21 @@ dict = {
inventory_hoverboards: `K-Drives`,
inventory_moaPets: `Moa`,
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`,

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: `Завершить все квесты`,

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: `完成所有任务`,