chore(webui): improving inconsistent, long string #2344
@ -1,4 +1,4 @@
 | 
			
		||||
FROM node:18-alpine3.19
 | 
			
		||||
FROM node:24-alpine3.21
 | 
			
		||||
 | 
			
		||||
ENV APP_MONGODB_URL=mongodb://mongodb:27017/openWF
 | 
			
		||||
ENV APP_MY_ADDRESS=localhost
 | 
			
		||||
 | 
			
		||||
@ -33,3 +33,4 @@ SpaceNinjaServer requires a `config.json`. To set it up, you can copy the [confi
 | 
			
		||||
  - `RadioLegion2Syndicate` for The Emissary
 | 
			
		||||
  - `RadioLegionIntermissionSyndicate` for Intermission I
 | 
			
		||||
  - `RadioLegionSyndicate` for The Wolf of Saturn Six
 | 
			
		||||
- `worldState.circuitGameModes` can be provided with an array of valid game modes (`Survival`, `VoidFlood`, `Excavation`, `Defense`, `Exterminate`, `Assassination`, `Alchemy`)
 | 
			
		||||
 | 
			
		||||
@ -58,7 +58,8 @@
 | 
			
		||||
    "starDays": true,
 | 
			
		||||
    "eidolonOverride": "",
 | 
			
		||||
    "vallisOverride": "",
 | 
			
		||||
    "nightwaveOverride": ""
 | 
			
		||||
    "nightwaveOverride": "",
 | 
			
		||||
    "circuitGameModes": null
 | 
			
		||||
  },
 | 
			
		||||
  "dev": {
 | 
			
		||||
    "keepVendorsExpired": false
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										8
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										8
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@ -18,7 +18,7 @@
 | 
			
		||||
        "morgan": "^1.10.0",
 | 
			
		||||
        "ncp": "^2.0.0",
 | 
			
		||||
        "typescript": "^5.5",
 | 
			
		||||
        "warframe-public-export-plus": "^0.5.67",
 | 
			
		||||
        "warframe-public-export-plus": "^0.5.68",
 | 
			
		||||
        "warframe-riven-info": "^0.1.2",
 | 
			
		||||
        "winston": "^3.17.0",
 | 
			
		||||
        "winston-daily-rotate-file": "^5.0.0"
 | 
			
		||||
@ -3814,9 +3814,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/warframe-public-export-plus": {
 | 
			
		||||
      "version": "0.5.67",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.67.tgz",
 | 
			
		||||
      "integrity": "sha512-LsnZD2E5PTA+5MK9kDGvM/hFDtg8sb0EwQ4hKH5ILqrSgz30a9W8785v77RSsL1AEVF8dfb/lZcSTCJq1DZHzQ=="
 | 
			
		||||
      "version": "0.5.68",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.68.tgz",
 | 
			
		||||
      "integrity": "sha512-KMmwCVeQ4k+EN73UZqxnM+qQdPsST8geWoJCP7US5LT6JcRxa8ptmqYXwCzaLtckBLZyVbamsxKZAxPPJckxsA=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/warframe-riven-info": {
 | 
			
		||||
      "version": "0.1.2",
 | 
			
		||||
 | 
			
		||||
@ -25,7 +25,7 @@
 | 
			
		||||
    "morgan": "^1.10.0",
 | 
			
		||||
    "ncp": "^2.0.0",
 | 
			
		||||
    "typescript": "^5.5",
 | 
			
		||||
    "warframe-public-export-plus": "^0.5.67",
 | 
			
		||||
    "warframe-public-export-plus": "^0.5.68",
 | 
			
		||||
    "warframe-riven-info": "^0.1.2",
 | 
			
		||||
    "winston": "^3.17.0",
 | 
			
		||||
    "winston-daily-rotate-file": "^5.0.0"
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										107
									
								
								src/controllers/api/crewShipFusionController.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								src/controllers/api/crewShipFusionController.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,107 @@
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { addMiscItems, freeUpSlot, getInventory, updateCurrency } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import { IOid } from "@/src/types/commonTypes";
 | 
			
		||||
import { ICrewShipComponentFingerprint, InventorySlot } from "@/src/types/inventoryTypes/inventoryTypes";
 | 
			
		||||
import { IInventoryChanges } from "@/src/types/purchaseTypes";
 | 
			
		||||
import { RequestHandler } from "express";
 | 
			
		||||
import { ExportCustoms, ExportDojoRecipes } from "warframe-public-export-plus";
 | 
			
		||||
 | 
			
		||||
export const crewShipFusionController: RequestHandler = async (req, res) => {
 | 
			
		||||
    const accountId = await getAccountIdForRequest(req);
 | 
			
		||||
    const inventory = await getInventory(accountId);
 | 
			
		||||
    const payload = getJSONfromString<ICrewShipFusionRequest>(String(req.body));
 | 
			
		||||
 | 
			
		||||
    const isWeapon = inventory.CrewShipWeapons.id(payload.PartA.$oid);
 | 
			
		||||
    const itemA = isWeapon ?? inventory.CrewShipWeaponSkins.id(payload.PartA.$oid)!;
 | 
			
		||||
    const category = isWeapon ? "CrewShipWeapons" : "CrewShipWeaponSkins";
 | 
			
		||||
    const salvageCategory = isWeapon ? "CrewShipSalvagedWeapons" : "CrewShipSalvagedWeaponSkins";
 | 
			
		||||
    const itemB = inventory[payload.SourceRecipe ? salvageCategory : category].id(payload.PartB.$oid)!;
 | 
			
		||||
    const tierA = itemA.ItemType.charCodeAt(itemA.ItemType.length - 1) - 65;
 | 
			
		||||
    const tierB = itemB.ItemType.charCodeAt(itemB.ItemType.length - 1) - 65;
 | 
			
		||||
 | 
			
		||||
    const inventoryChanges: IInventoryChanges = {};
 | 
			
		||||
 | 
			
		||||
    // Charge partial repair cost if fusing with an identified but unrepaired part
 | 
			
		||||
    if (payload.SourceRecipe) {
 | 
			
		||||
        const recipe = ExportDojoRecipes.research[payload.SourceRecipe];
 | 
			
		||||
        updateCurrency(inventory, Math.round(recipe.price * 0.4), false, inventoryChanges);
 | 
			
		||||
        const miscItemChanges = recipe.ingredients.map(x => ({ ...x, ItemCount: Math.round(x.ItemCount * -0.4) }));
 | 
			
		||||
        addMiscItems(inventory, miscItemChanges);
 | 
			
		||||
        inventoryChanges.MiscItems = miscItemChanges;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Remove inferior item
 | 
			
		||||
    if (payload.SourceRecipe) {
 | 
			
		||||
        inventory[salvageCategory].pull({ _id: payload.PartB.$oid });
 | 
			
		||||
        inventoryChanges.RemovedIdItems = [{ ItemId: payload.PartB }];
 | 
			
		||||
    } else {
 | 
			
		||||
        const inferiorId = tierA < tierB ? payload.PartA : payload.PartB;
 | 
			
		||||
        inventory[category].pull({ _id: inferiorId.$oid });
 | 
			
		||||
        inventoryChanges.RemovedIdItems = [{ ItemId: inferiorId }];
 | 
			
		||||
        freeUpSlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS);
 | 
			
		||||
        inventoryChanges[InventorySlot.RJ_COMPONENT_AND_ARMAMENTS] = { count: -1, platinum: 0, Slots: 1 };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Upgrade superior item
 | 
			
		||||
    const superiorItem = tierA < tierB ? itemB : itemA;
 | 
			
		||||
    const inferiorItem = tierA < tierB ? itemA : itemB;
 | 
			
		||||
    const fingerprint: ICrewShipComponentFingerprint = JSON.parse(
 | 
			
		||||
        superiorItem.UpgradeFingerprint!
 | 
			
		||||
    ) as ICrewShipComponentFingerprint;
 | 
			
		||||
    const inferiorFingerprint: ICrewShipComponentFingerprint = inferiorItem.UpgradeFingerprint
 | 
			
		||||
        ? (JSON.parse(inferiorItem.UpgradeFingerprint) as ICrewShipComponentFingerprint)
 | 
			
		||||
        : { compat: "", buffs: [] };
 | 
			
		||||
    if (isWeapon) {
 | 
			
		||||
        for (let i = 0; i != fingerprint.buffs.length; ++i) {
 | 
			
		||||
            const buffA = fingerprint.buffs[i];
 | 
			
		||||
            const buffB = i < inferiorFingerprint.buffs.length ? inferiorFingerprint.buffs[i] : undefined;
 | 
			
		||||
            const fvalA = buffA.Value / 0x3fffffff;
 | 
			
		||||
            const fvalB = (buffB?.Value ?? 0) / 0x3fffffff;
 | 
			
		||||
            const percA = 0.3 + fvalA * (0.6 - 0.3);
 | 
			
		||||
            const percB = 0.3 + fvalB * (0.6 - 0.3);
 | 
			
		||||
            const newPerc = Math.min(0.6, Math.max(percA, percB) * FUSE_MULTIPLIERS[Math.abs(tierA - tierB)]);
 | 
			
		||||
            const newFval = (newPerc - 0.3) / (0.6 - 0.3);
 | 
			
		||||
            buffA.Value = Math.trunc(newFval * 0x3fffffff);
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        const superiorMeta = ExportCustoms[superiorItem.ItemType].randomisedUpgrades ?? [];
 | 
			
		||||
        const inferiorMeta = ExportCustoms[inferiorItem.ItemType].randomisedUpgrades ?? [];
 | 
			
		||||
        for (let i = 0; i != inferiorFingerprint.buffs.length; ++i) {
 | 
			
		||||
            const buffA = fingerprint.buffs[i];
 | 
			
		||||
            const buffB = inferiorFingerprint.buffs[i];
 | 
			
		||||
            const fvalA = buffA.Value / 0x3fffffff;
 | 
			
		||||
            const fvalB = buffB.Value / 0x3fffffff;
 | 
			
		||||
            const rangeA = superiorMeta[i].range;
 | 
			
		||||
            const rangeB = inferiorMeta[i].range;
 | 
			
		||||
            const percA = rangeA[0] + fvalA * (rangeA[1] - rangeA[0]);
 | 
			
		||||
            const percB = rangeB[0] + fvalB * (rangeB[1] - rangeB[0]);
 | 
			
		||||
            const newPerc = Math.min(rangeA[1], Math.max(percA, percB) * FUSE_MULTIPLIERS[Math.abs(tierA - tierB)]);
 | 
			
		||||
            const newFval = (newPerc - rangeA[0]) / (rangeA[1] - rangeA[0]);
 | 
			
		||||
            buffA.Value = Math.trunc(newFval * 0x3fffffff);
 | 
			
		||||
        }
 | 
			
		||||
        if (inferiorFingerprint.SubroutineIndex) {
 | 
			
		||||
            const useSuperiorSubroutine = tierA < tierB ? !payload.UseSubroutineA : payload.UseSubroutineA;
 | 
			
		||||
            if (!useSuperiorSubroutine) {
 | 
			
		||||
                fingerprint.SubroutineIndex = inferiorFingerprint.SubroutineIndex;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    superiorItem.UpgradeFingerprint = JSON.stringify(fingerprint);
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
    inventoryChanges[category] = [superiorItem.toJSON() as any];
 | 
			
		||||
 | 
			
		||||
    await inventory.save();
 | 
			
		||||
    res.json({
 | 
			
		||||
        InventoryChanges: inventoryChanges
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface ICrewShipFusionRequest {
 | 
			
		||||
    PartA: IOid;
 | 
			
		||||
    PartB: IOid;
 | 
			
		||||
    SourceRecipe: string;
 | 
			
		||||
    UseSubroutineA: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const FUSE_MULTIPLIERS = [1.1, 1.05, 1.02];
 | 
			
		||||
@ -11,7 +11,7 @@ import {
 | 
			
		||||
    scaleRequiredCount,
 | 
			
		||||
    setGuildTechLogState
 | 
			
		||||
} from "@/src/services/guildService";
 | 
			
		||||
import { ExportDojoRecipes } from "warframe-public-export-plus";
 | 
			
		||||
import { ExportDojoRecipes, ExportRailjackWeapons } from "warframe-public-export-plus";
 | 
			
		||||
import { getAccountIdForRequest } from "@/src/services/loginService";
 | 
			
		||||
import {
 | 
			
		||||
    addCrewShipWeaponSkin,
 | 
			
		||||
@ -442,6 +442,7 @@ const finishComponentRepair = (
 | 
			
		||||
        ...(category == "CrewShipWeaponSkins"
 | 
			
		||||
            ? addCrewShipWeaponSkin(inventory, salvageItem.ItemType, salvageItem.UpgradeFingerprint)
 | 
			
		||||
            : addEquipment(inventory, category, salvageItem.ItemType, {
 | 
			
		||||
                  UpgradeType: ExportRailjackWeapons[salvageItem.ItemType].defaultUpgrades?.[0].ItemType,
 | 
			
		||||
                  UpgradeFingerprint: salvageItem.UpgradeFingerprint
 | 
			
		||||
              })),
 | 
			
		||||
        ...occupySlot(inventory, InventorySlot.RJ_COMPONENT_AND_ARMAMENTS, false)
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,7 @@ import {
 | 
			
		||||
    IKnifeResponse
 | 
			
		||||
} from "@/src/helpers/nemesisHelpers";
 | 
			
		||||
import { getJSONfromString } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
 | 
			
		||||
import { Loadout } from "@/src/models/inventoryModels/loadoutModel";
 | 
			
		||||
import { freeUpSlot, getInventory } from "@/src/services/inventoryService";
 | 
			
		||||
import { getAccountForRequest } from "@/src/services/loginService";
 | 
			
		||||
@ -202,16 +203,28 @@ export const nemesisController: RequestHandler = async (req, res) => {
 | 
			
		||||
            guess[body.position].result = correct ? GUESS_CORRECT : GUESS_INCORRECT;
 | 
			
		||||
            inventory.Nemesis!.GuessHistory[inventory.Nemesis!.GuessHistory.length - 1] = encodeNemesisGuess(guess);
 | 
			
		||||
 | 
			
		||||
            // Increase rank if incorrect
 | 
			
		||||
            let RankIncrease: number | undefined;
 | 
			
		||||
            if (!correct) {
 | 
			
		||||
                RankIncrease = 1;
 | 
			
		||||
            const response: INemesisRequiemResponse = {};
 | 
			
		||||
            if (correct) {
 | 
			
		||||
                if (body.position == 2) {
 | 
			
		||||
                    // That was all 3 guesses correct, nemesis is now weakened.
 | 
			
		||||
                    inventory.Nemesis!.InfNodes = [
 | 
			
		||||
                        {
 | 
			
		||||
                            Node: getNemesisManifest(inventory.Nemesis!.manifest).showdownNode,
 | 
			
		||||
                            Influence: 1
 | 
			
		||||
                        }
 | 
			
		||||
                    ];
 | 
			
		||||
                    inventory.Nemesis!.Weakened = true;
 | 
			
		||||
                    await consumePasscodeModCharges(inventory, response);
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                // Guess was incorrect, increase rank
 | 
			
		||||
                response.RankIncrease = 1;
 | 
			
		||||
                const manifest = getNemesisManifest(inventory.Nemesis!.manifest);
 | 
			
		||||
                inventory.Nemesis!.Rank = Math.min(inventory.Nemesis!.Rank + 1, manifest.systemIndexes.length - 1);
 | 
			
		||||
                inventory.Nemesis!.InfNodes = getInfNodes(manifest, inventory.Nemesis!.Rank);
 | 
			
		||||
            }
 | 
			
		||||
            await inventory.save();
 | 
			
		||||
            res.json({ RankIncrease });
 | 
			
		||||
            res.json(response);
 | 
			
		||||
        }
 | 
			
		||||
    } else if ((req.query.mode as string) == "rs") {
 | 
			
		||||
        // report spawn; POST but no application data in body
 | 
			
		||||
@ -299,20 +312,11 @@ export const nemesisController: RequestHandler = async (req, res) => {
 | 
			
		||||
        ];
 | 
			
		||||
        inventory.Nemesis!.Weakened = true;
 | 
			
		||||
 | 
			
		||||
        const response: IKnifeResponse & { target: INemesisClient } = {
 | 
			
		||||
        const response: INemesisWeakenResponse = {
 | 
			
		||||
            target: inventory.toJSON<IInventoryClient>().Nemesis!
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // Consume charge of the correct requiem mod(s)
 | 
			
		||||
        const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!;
 | 
			
		||||
        const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid);
 | 
			
		||||
        const dataknifeConfigIndex = dataknifeLoadout?.s?.mod ?? 0;
 | 
			
		||||
        const dataknifeUpgrades = inventory.DataKnives[0].Configs[dataknifeConfigIndex].Upgrades!;
 | 
			
		||||
        const modTypes = getNemesisPasscodeModTypes(inventory.Nemesis!);
 | 
			
		||||
        for (const modType of modTypes) {
 | 
			
		||||
            const upgrade = getKnifeUpgrade(inventory, dataknifeUpgrades, modType);
 | 
			
		||||
            consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
 | 
			
		||||
        }
 | 
			
		||||
        await consumePasscodeModCharges(inventory, response);
 | 
			
		||||
 | 
			
		||||
        await inventory.save();
 | 
			
		||||
        res.json(response);
 | 
			
		||||
@ -370,11 +374,19 @@ interface INemesisRequiemRequest {
 | 
			
		||||
    knife?: IKnife;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface INemesisRequiemResponse extends IKnifeResponse {
 | 
			
		||||
    RankIncrease?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// interface INemesisWeakenRequest {
 | 
			
		||||
//     target: INemesisClient;
 | 
			
		||||
//     knife: IKnife;
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
interface INemesisWeakenResponse extends IKnifeResponse {
 | 
			
		||||
    target: INemesisClient;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IKnife {
 | 
			
		||||
    Item: IEquipmentClient;
 | 
			
		||||
    Skins: IWeaponSkinClient[];
 | 
			
		||||
@ -383,3 +395,18 @@ interface IKnife {
 | 
			
		||||
    AttachedUpgrades: IUpgradeClient[];
 | 
			
		||||
    HiddenWhenHolstered: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const consumePasscodeModCharges = async (
 | 
			
		||||
    inventory: TInventoryDatabaseDocument,
 | 
			
		||||
    response: IKnifeResponse
 | 
			
		||||
): Promise<void> => {
 | 
			
		||||
    const loadout = (await Loadout.findById(inventory.LoadOutPresets, "DATAKNIFE"))!;
 | 
			
		||||
    const dataknifeLoadout = loadout.DATAKNIFE.id(inventory.CurrentLoadOutIds[LoadoutIndex.DATAKNIFE].$oid);
 | 
			
		||||
    const dataknifeConfigIndex = dataknifeLoadout?.s?.mod ?? 0;
 | 
			
		||||
    const dataknifeUpgrades = inventory.DataKnives[0].Configs[dataknifeConfigIndex].Upgrades!;
 | 
			
		||||
    const modTypes = getNemesisPasscodeModTypes(inventory.Nemesis!);
 | 
			
		||||
    for (const modType of modTypes) {
 | 
			
		||||
        const upgrade = getKnifeUpgrade(inventory, dataknifeUpgrades, modType);
 | 
			
		||||
        consumeModCharge(response, inventory, upgrade, dataknifeUpgrades);
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -11,8 +11,11 @@ export const updateChallengeProgressController: RequestHandler = async (req, res
 | 
			
		||||
 | 
			
		||||
    const inventory = await getInventory(
 | 
			
		||||
        account._id.toString(),
 | 
			
		||||
        "ChallengeProgress SeasonChallengeHistory Affiliations"
 | 
			
		||||
        "ChallengesFixVersion ChallengeProgress SeasonChallengeHistory Affiliations"
 | 
			
		||||
    );
 | 
			
		||||
    if (challenges.ChallengesFixVersion !== undefined) {
 | 
			
		||||
        inventory.ChallengesFixVersion = challenges.ChallengesFixVersion;
 | 
			
		||||
    }
 | 
			
		||||
    let affiliationMods: IAffiliationMods[] = [];
 | 
			
		||||
    if (challenges.ChallengeProgress) {
 | 
			
		||||
        affiliationMods = addChallenges(
 | 
			
		||||
@ -40,6 +43,7 @@ export const updateChallengeProgressController: RequestHandler = async (req, res
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
interface IUpdateChallengeProgressRequest {
 | 
			
		||||
    ChallengesFixVersion?: number;
 | 
			
		||||
    ChallengeProgress?: IChallengeProgress[];
 | 
			
		||||
    SeasonChallengeHistory?: ISeasonChallenge[];
 | 
			
		||||
    SeasonChallengeCompletions?: ISeasonChallenge[];
 | 
			
		||||
 | 
			
		||||
@ -1703,7 +1703,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
 | 
			
		||||
        LastInventorySync: Schema.Types.ObjectId,
 | 
			
		||||
        Mailbox: MailboxSchema,
 | 
			
		||||
        HandlerPoints: Number,
 | 
			
		||||
        ChallengesFixVersion: { type: Number, default: 6 },
 | 
			
		||||
        ChallengesFixVersion: Number,
 | 
			
		||||
        PlayedParkourTutorial: Boolean,
 | 
			
		||||
        //ActiveLandscapeTraps: [Schema.Types.Mixed],
 | 
			
		||||
        //RepVotes: [Schema.Types.Mixed],
 | 
			
		||||
 | 
			
		||||
@ -33,6 +33,7 @@ import { createAllianceController } from "@/src/controllers/api/createAllianceCo
 | 
			
		||||
import { createGuildController } from "@/src/controllers/api/createGuildController";
 | 
			
		||||
import { creditsController } from "@/src/controllers/api/creditsController";
 | 
			
		||||
import { crewMembersController } from "@/src/controllers/api/crewMembersController";
 | 
			
		||||
import { crewShipFusionController } from "@/src/controllers/api/crewShipFusionController";
 | 
			
		||||
import { crewShipIdentifySalvageController } from "@/src/controllers/api/crewShipIdentifySalvageController";
 | 
			
		||||
import { customizeGuildRanksController } from "@/src/controllers/api/customizeGuildRanksController";
 | 
			
		||||
import { customObstacleCourseLeaderboardController } from "@/src/controllers/api/customObstacleCourseLeaderboardController";
 | 
			
		||||
@ -247,6 +248,7 @@ apiRouter.post("/contributeToVault.php", contributeToVaultController);
 | 
			
		||||
apiRouter.post("/createAlliance.php", createAllianceController);
 | 
			
		||||
apiRouter.post("/createGuild.php", createGuildController);
 | 
			
		||||
apiRouter.post("/crewMembers.php", crewMembersController);
 | 
			
		||||
apiRouter.post("/crewShipFusion.php", crewShipFusionController);
 | 
			
		||||
apiRouter.post("/crewShipIdentifySalvage.php", crewShipIdentifySalvageController);
 | 
			
		||||
apiRouter.post("/customizeGuildRanks.php", customizeGuildRanksController);
 | 
			
		||||
apiRouter.post("/customObstacleCourseLeaderboard.php", customObstacleCourseLeaderboardController);
 | 
			
		||||
 | 
			
		||||
@ -65,6 +65,7 @@ interface IConfig {
 | 
			
		||||
        eidolonOverride?: string;
 | 
			
		||||
        vallisOverride?: string;
 | 
			
		||||
        nightwaveOverride?: string;
 | 
			
		||||
        circuitGameModes?: string[];
 | 
			
		||||
    };
 | 
			
		||||
    dev?: {
 | 
			
		||||
        keepVendorsExpired?: boolean;
 | 
			
		||||
 | 
			
		||||
@ -50,14 +50,17 @@ export const createNewEventMessages = async (req: Request): Promise<void> => {
 | 
			
		||||
    await account.save();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const createMessage = async (accountId: string | Types.ObjectId, messages: IMessageCreationTemplate[]) => {
 | 
			
		||||
export const createMessage = async (
 | 
			
		||||
    accountId: string | Types.ObjectId,
 | 
			
		||||
    messages: IMessageCreationTemplate[]
 | 
			
		||||
): Promise<HydratedDocument<IMessageDatabase>[]> => {
 | 
			
		||||
    const ownerIdMessages = messages.map(m => ({
 | 
			
		||||
        ...m,
 | 
			
		||||
        ownerId: accountId
 | 
			
		||||
    }));
 | 
			
		||||
 | 
			
		||||
    const savedMessages = await Inbox.insertMany(ownerIdMessages);
 | 
			
		||||
    return savedMessages;
 | 
			
		||||
    return savedMessages as HydratedDocument<IMessageDatabase>[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface IMessageCreationTemplate extends Omit<IMessageDatabase, "_id" | "date" | "ownerId"> {
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,6 @@ import { isDev } from "@/src/helpers/pathHelper";
 | 
			
		||||
import { catBreadHash } from "@/src/helpers/stringHelpers";
 | 
			
		||||
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
 | 
			
		||||
import { mixSeeds, SRng } from "@/src/services/rngService";
 | 
			
		||||
import { IMongoDate } from "@/src/types/commonTypes";
 | 
			
		||||
import { IItemManifest, IVendorInfo, IVendorManifest } from "@/src/types/vendorTypes";
 | 
			
		||||
import { logger } from "@/src/utils/logger";
 | 
			
		||||
import { ExportVendors, IRange, IVendor, IVendorOffer } from "warframe-public-export-plus";
 | 
			
		||||
@ -25,7 +24,6 @@ import Nova1999ConquestShopManifest from "@/static/fixed_responses/getVendorInfo
 | 
			
		||||
import OstronPetVendorManifest from "@/static/fixed_responses/getVendorInfo/OstronPetVendorManifest.json";
 | 
			
		||||
import SolarisDebtTokenVendorRepossessionsManifest from "@/static/fixed_responses/getVendorInfo/SolarisDebtTokenVendorRepossessionsManifest.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: IVendorManifest[] = [
 | 
			
		||||
@ -46,7 +44,6 @@ const rawVendorManifests: IVendorManifest[] = [
 | 
			
		||||
    OstronPetVendorManifest,
 | 
			
		||||
    SolarisDebtTokenVendorRepossessionsManifest,
 | 
			
		||||
    Temple1999VendorManifest,
 | 
			
		||||
    TeshinHardModeVendorManifest, // uses preprocessing
 | 
			
		||||
    ZarimanCommisionsManifestArchimedean
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@ -87,12 +84,16 @@ const gcd = (a: number, b: number): number => {
 | 
			
		||||
const getCycleDuration = (manifest: IVendor): number => {
 | 
			
		||||
    let dur = 0;
 | 
			
		||||
    for (const item of manifest.items) {
 | 
			
		||||
        if (typeof item.durationHours != "number") {
 | 
			
		||||
        if (item.alwaysOffered) {
 | 
			
		||||
            continue;
 | 
			
		||||
        }
 | 
			
		||||
        const durationHours = item.rotatedWeekly ? 168 : item.durationHours;
 | 
			
		||||
        if (typeof durationHours != "number") {
 | 
			
		||||
            dur = 1;
 | 
			
		||||
            break;
 | 
			
		||||
        }
 | 
			
		||||
        if (dur != item.durationHours) {
 | 
			
		||||
            dur = gcd(dur, item.durationHours);
 | 
			
		||||
        if (dur != durationHours) {
 | 
			
		||||
            dur = gcd(dur, durationHours);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return dur * unixTimesInMs.hour;
 | 
			
		||||
@ -101,7 +102,7 @@ const getCycleDuration = (manifest: IVendor): number => {
 | 
			
		||||
export const getVendorManifestByTypeName = (typeName: string): IVendorManifest | undefined => {
 | 
			
		||||
    for (const vendorManifest of rawVendorManifests) {
 | 
			
		||||
        if (vendorManifest.VendorInfo.TypeName == typeName) {
 | 
			
		||||
            return preprocessVendorManifest(vendorManifest);
 | 
			
		||||
            return vendorManifest;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    for (const vendorInfo of generatableVendors) {
 | 
			
		||||
@ -124,7 +125,7 @@ export const getVendorManifestByTypeName = (typeName: string): IVendorManifest |
 | 
			
		||||
export const getVendorManifestByOid = (oid: string): IVendorManifest | undefined => {
 | 
			
		||||
    for (const vendorManifest of rawVendorManifests) {
 | 
			
		||||
        if (vendorManifest.VendorInfo._id.$oid == oid) {
 | 
			
		||||
            return preprocessVendorManifest(vendorManifest);
 | 
			
		||||
            return vendorManifest;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    for (const vendorInfo of generatableVendors) {
 | 
			
		||||
@ -183,30 +184,6 @@ export const applyStandingToVendorManifest = (
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
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) {
 | 
			
		||||
            refreshExpiry(offer.Expiry);
 | 
			
		||||
        }
 | 
			
		||||
        return manifest;
 | 
			
		||||
    }
 | 
			
		||||
    return originalManifest;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const refreshExpiry = (expiry: IMongoDate): void => {
 | 
			
		||||
    const period = parseInt(expiry.$date.$numberLong);
 | 
			
		||||
    if (Date.now() >= period) {
 | 
			
		||||
        const epoch = 1734307200_000; // Monday (for weekly schedules)
 | 
			
		||||
        const iteration = Math.trunc((Date.now() - epoch) / period);
 | 
			
		||||
        const start = epoch + iteration * period;
 | 
			
		||||
        const end = start + period;
 | 
			
		||||
        expiry.$date.$numberLong = end.toString();
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const toRange = (value: IRange | number): IRange => {
 | 
			
		||||
    if (typeof value == "number") {
 | 
			
		||||
        return { minValue: value, maxValue: value };
 | 
			
		||||
@ -230,6 +207,18 @@ const getCycleDurationRange = (manifest: IVendor): IRange | undefined => {
 | 
			
		||||
    return res.maxValue != 0 ? res : undefined;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type TOfferId = string;
 | 
			
		||||
 | 
			
		||||
const getOfferId = (offer: IVendorOffer | IItemManifest): TOfferId => {
 | 
			
		||||
    if ("storeItem" in offer) {
 | 
			
		||||
        // IVendorOffer
 | 
			
		||||
        return offer.storeItem + "x" + offer.quantity;
 | 
			
		||||
    } else {
 | 
			
		||||
        // IItemManifest
 | 
			
		||||
        return offer.StoreItem + "x" + offer.QuantityMultiplier;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const vendorManifestCache: Record<string, IVendorManifest> = {};
 | 
			
		||||
 | 
			
		||||
const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorManifest => {
 | 
			
		||||
@ -270,7 +259,8 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
 | 
			
		||||
        const rng = new SRng(mixSeeds(vendorSeed, cycleIndex));
 | 
			
		||||
        const offersToAdd: IVendorOffer[] = [];
 | 
			
		||||
        if (!manifest.isOneBinPerCycle) {
 | 
			
		||||
            const remainingItemCapacity: Record<string, number> = {};
 | 
			
		||||
            // Compute vendor requirements, subtracting existing offers
 | 
			
		||||
            const remainingItemCapacity: Record<TOfferId, number> = {};
 | 
			
		||||
            const missingItemsPerBin: Record<number, number> = {};
 | 
			
		||||
            let numOffersThatNeedToMatchABin = 0;
 | 
			
		||||
            if (manifest.numItemsPerBin) {
 | 
			
		||||
@ -280,56 +270,59 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            for (const item of manifest.items) {
 | 
			
		||||
                remainingItemCapacity[item.storeItem] = 1 + item.duplicates;
 | 
			
		||||
                remainingItemCapacity[getOfferId(item)] = 1 + item.duplicates;
 | 
			
		||||
            }
 | 
			
		||||
            for (const offer of info.ItemManifest) {
 | 
			
		||||
                remainingItemCapacity[offer.StoreItem] -= 1;
 | 
			
		||||
                remainingItemCapacity[getOfferId(offer)] -= 1;
 | 
			
		||||
                const bin = parseInt(offer.Bin.substring(4));
 | 
			
		||||
                if (missingItemsPerBin[bin]) {
 | 
			
		||||
                    missingItemsPerBin[bin] -= 1;
 | 
			
		||||
                    numOffersThatNeedToMatchABin -= 1;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            if (manifest.numItems && manifest.items.length != manifest.numItems.minValue) {
 | 
			
		||||
                const numItemsTarget = rng.randomInt(manifest.numItems.minValue, manifest.numItems.maxValue);
 | 
			
		||||
 | 
			
		||||
            // Add permanent offers
 | 
			
		||||
            let numUncountedOffers = 0;
 | 
			
		||||
            let offset = 0;
 | 
			
		||||
            for (const item of manifest.items) {
 | 
			
		||||
                if (item.alwaysOffered || item.rotatedWeekly) {
 | 
			
		||||
                    ++numUncountedOffers;
 | 
			
		||||
                    const id = getOfferId(item);
 | 
			
		||||
                    if (remainingItemCapacity[id] != 0) {
 | 
			
		||||
                        remainingItemCapacity[id] -= 1;
 | 
			
		||||
                        offersToAdd.push(item);
 | 
			
		||||
                        ++offset;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Add counted offers
 | 
			
		||||
            if (manifest.numItems) {
 | 
			
		||||
                const useRng = manifest.numItems.minValue != manifest.numItems.maxValue;
 | 
			
		||||
                const numItemsTarget =
 | 
			
		||||
                    numUncountedOffers +
 | 
			
		||||
                    (useRng
 | 
			
		||||
                        ? rng.randomInt(manifest.numItems.minValue, manifest.numItems.maxValue)
 | 
			
		||||
                        : manifest.numItems.minValue);
 | 
			
		||||
                let i = 0;
 | 
			
		||||
                while (info.ItemManifest.length + offersToAdd.length < numItemsTarget) {
 | 
			
		||||
                    // TODO: Consider item probability weightings
 | 
			
		||||
                    const item = rng.randomElement(manifest.items)!;
 | 
			
		||||
                    const item = useRng ? rng.randomElement(manifest.items)! : manifest.items[i++];
 | 
			
		||||
                    if (
 | 
			
		||||
                        remainingItemCapacity[item.storeItem] != 0 &&
 | 
			
		||||
                        !item.alwaysOffered &&
 | 
			
		||||
                        remainingItemCapacity[getOfferId(item)] != 0 &&
 | 
			
		||||
                        (numOffersThatNeedToMatchABin == 0 || missingItemsPerBin[item.bin])
 | 
			
		||||
                    ) {
 | 
			
		||||
                        remainingItemCapacity[item.storeItem] -= 1;
 | 
			
		||||
                        remainingItemCapacity[getOfferId(item)] -= 1;
 | 
			
		||||
                        if (missingItemsPerBin[item.bin]) {
 | 
			
		||||
                            missingItemsPerBin[item.bin] -= 1;
 | 
			
		||||
                            numOffersThatNeedToMatchABin -= 1;
 | 
			
		||||
                        }
 | 
			
		||||
                        offersToAdd.push(item);
 | 
			
		||||
                        offersToAdd.splice(offset, 0, item);
 | 
			
		||||
                    }
 | 
			
		||||
                    if (i == manifest.items.length) {
 | 
			
		||||
                        i = 0;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                for (const item of manifest.items) {
 | 
			
		||||
                    if (!item.alwaysOffered && remainingItemCapacity[item.storeItem] != 0) {
 | 
			
		||||
                        remainingItemCapacity[item.storeItem] -= 1;
 | 
			
		||||
                        offersToAdd.push(item);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                for (const e of Object.entries(remainingItemCapacity)) {
 | 
			
		||||
                    const item = manifest.items.find(x => x.storeItem == e[0])!;
 | 
			
		||||
                    if (!item.alwaysOffered) {
 | 
			
		||||
                        while (e[1] != 0) {
 | 
			
		||||
                            e[1] -= 1;
 | 
			
		||||
                            offersToAdd.push(item);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                for (const item of manifest.items) {
 | 
			
		||||
                    if (item.alwaysOffered && remainingItemCapacity[item.storeItem] != 0) {
 | 
			
		||||
                        remainingItemCapacity[item.storeItem] -= 1;
 | 
			
		||||
                        offersToAdd.push(item);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                offersToAdd.reverse();
 | 
			
		||||
            }
 | 
			
		||||
        } else {
 | 
			
		||||
            const binThisCycle = cycleIndex % 2; // Note: May want to auto-compute the bin size, but this is only used for coda weapons right now.
 | 
			
		||||
@ -342,16 +335,21 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
 | 
			
		||||
        const cycleStart = cycleOffset + cycleIndex * cycleDuration;
 | 
			
		||||
        for (const rawItem of offersToAdd) {
 | 
			
		||||
            const durationHoursRange = toRange(rawItem.durationHours ?? cycleDuration);
 | 
			
		||||
            const expiry =
 | 
			
		||||
                cycleStart +
 | 
			
		||||
                rng.randomInt(durationHoursRange.minValue, durationHoursRange.maxValue) * unixTimesInMs.hour;
 | 
			
		||||
            const expiry = rawItem.alwaysOffered
 | 
			
		||||
                ? 2051240400_000
 | 
			
		||||
                : cycleStart +
 | 
			
		||||
                  (rawItem.rotatedWeekly
 | 
			
		||||
                      ? unixTimesInMs.week
 | 
			
		||||
                      : rng.randomInt(durationHoursRange.minValue, durationHoursRange.maxValue) * unixTimesInMs.hour);
 | 
			
		||||
            const item: IItemManifest = {
 | 
			
		||||
                StoreItem: rawItem.storeItem,
 | 
			
		||||
                ItemPrices: rawItem.itemPrices?.map(itemPrice => ({ ...itemPrice, ProductCategory: "MiscItems" })),
 | 
			
		||||
                Bin: "BIN_" + rawItem.bin,
 | 
			
		||||
                QuantityMultiplier: rawItem.quantity,
 | 
			
		||||
                Expiry: { $date: { $numberLong: expiry.toString() } },
 | 
			
		||||
                AllowMultipurchase: false,
 | 
			
		||||
                PurchaseQuantityLimit: rawItem.purchaseLimit,
 | 
			
		||||
                RotatedWeekly: rawItem.rotatedWeekly,
 | 
			
		||||
                AllowMultipurchase: rawItem.purchaseLimit !== 1,
 | 
			
		||||
                Id: {
 | 
			
		||||
                    $oid:
 | 
			
		||||
                        ((cycleStart / 1000) & 0xffffffff).toString(16).padStart(8, "0") +
 | 
			
		||||
@ -422,6 +420,13 @@ const generateVendorManifest = (vendorInfo: IGeneratableVendorInfo): IVendorMani
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
if (isDev) {
 | 
			
		||||
    if (
 | 
			
		||||
        getCycleDuration(ExportVendors["/Lotus/Types/Game/VendorManifests/Hubs/TeshinHardModeVendorManifest"]) !=
 | 
			
		||||
        unixTimesInMs.week
 | 
			
		||||
    ) {
 | 
			
		||||
        logger.warn(`getCycleDuration self test failed`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const ads = getVendorManifestByTypeName("/Lotus/Types/Game/VendorManifests/Hubs/GuildAdvertisementVendorManifest")!
 | 
			
		||||
        .VendorInfo.ItemManifest;
 | 
			
		||||
    if (
 | 
			
		||||
 | 
			
		||||
@ -16,8 +16,10 @@ import {
 | 
			
		||||
    ISortie,
 | 
			
		||||
    ISortieMission,
 | 
			
		||||
    ISyndicateMissionInfo,
 | 
			
		||||
    ITmp,
 | 
			
		||||
    IVoidStorm,
 | 
			
		||||
    IWorldState
 | 
			
		||||
    IWorldState,
 | 
			
		||||
    TCircuitGameMode
 | 
			
		||||
} from "../types/worldStateTypes";
 | 
			
		||||
import { version_compare } from "../helpers/inventoryHelpers";
 | 
			
		||||
import { logger } from "../utils/logger";
 | 
			
		||||
@ -1297,12 +1299,15 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
			
		||||
        pushVoidStorms(worldState.VoidStorms, hour);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Sentient Anomaly cycling every 30 minutes
 | 
			
		||||
    // Sentient Anomaly + Xtra Cheese cycles
 | 
			
		||||
    const halfHour = Math.trunc(timeMs / (unixTimesInMs.hour / 2));
 | 
			
		||||
    const tmp = {
 | 
			
		||||
    const hourInSeconds = 3600;
 | 
			
		||||
    const cheeseInterval = hourInSeconds * 8;
 | 
			
		||||
    const cheeseDuration = hourInSeconds * 2;
 | 
			
		||||
    const cheeseIndex = Math.trunc(timeSecs / cheeseInterval);
 | 
			
		||||
    const tmp: ITmp = {
 | 
			
		||||
        cavabegin: "1690761600",
 | 
			
		||||
        PurchasePlatformLockEnabled: true,
 | 
			
		||||
        tcsn: true,
 | 
			
		||||
        pgr: {
 | 
			
		||||
            ts: "1732572900",
 | 
			
		||||
            en: "CUSTOM DECALS @ ZEVILA",
 | 
			
		||||
@ -1323,8 +1328,16 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
 | 
			
		||||
        },
 | 
			
		||||
        ennnd: true,
 | 
			
		||||
        mbrt: true,
 | 
			
		||||
        fbst: {
 | 
			
		||||
            a: cheeseIndex * cheeseInterval, // This has a bug where the client shows a negative time for "Xtra cheese starts in ..." until it refreshes the world state. This is because we're only providing the new activation as soon as that time/date is reached. However, this is 100% faithful to live.
 | 
			
		||||
            e: cheeseIndex * cheeseInterval + cheeseDuration,
 | 
			
		||||
            n: (cheeseIndex + 1) * cheeseInterval
 | 
			
		||||
        },
 | 
			
		||||
        sfn: [550, 553, 554, 555][halfHour % 4]
 | 
			
		||||
    };
 | 
			
		||||
    if (Array.isArray(config.worldState?.circuitGameModes)) {
 | 
			
		||||
        tmp.edg = config.worldState.circuitGameModes as TCircuitGameMode[];
 | 
			
		||||
    }
 | 
			
		||||
    worldState.Tmp = JSON.stringify(tmp);
 | 
			
		||||
 | 
			
		||||
    return worldState;
 | 
			
		||||
 | 
			
		||||
@ -234,7 +234,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
 | 
			
		||||
    HandlerPoints: number;
 | 
			
		||||
    MiscItems: IMiscItem[];
 | 
			
		||||
    HasOwnedVoidProjectionsPreviously?: boolean;
 | 
			
		||||
    ChallengesFixVersion: number;
 | 
			
		||||
    ChallengesFixVersion?: number;
 | 
			
		||||
    ChallengeProgress: IChallengeProgress[];
 | 
			
		||||
    RawUpgrades: IRawUpgrade[];
 | 
			
		||||
    ReceivedStartingGear: boolean;
 | 
			
		||||
 | 
			
		||||
@ -191,3 +191,48 @@ export interface ICalendarEvent {
 | 
			
		||||
    dialogueName?: string;
 | 
			
		||||
    dialogueConvo?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type TCircuitGameMode =
 | 
			
		||||
    | "Survival"
 | 
			
		||||
    | "VoidFlood"
 | 
			
		||||
    | "Excavation"
 | 
			
		||||
    | "Defense"
 | 
			
		||||
    | "Exterminate"
 | 
			
		||||
    | "Assassination"
 | 
			
		||||
    | "Alchemy";
 | 
			
		||||
 | 
			
		||||
export interface ITmp {
 | 
			
		||||
    cavabegin: string;
 | 
			
		||||
    PurchasePlatformLockEnabled: boolean; // Seems unused
 | 
			
		||||
    pgr: IPgr;
 | 
			
		||||
    ennnd?: boolean; // True if 1999 demo is available (no effect for >=38.6.0)
 | 
			
		||||
    mbrt?: boolean; // Related to mobile app rating request
 | 
			
		||||
    fbst: IFbst;
 | 
			
		||||
    sfn: number;
 | 
			
		||||
    edg?: TCircuitGameMode[]; // The Circuit game modes overwrite
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IPgr {
 | 
			
		||||
    ts: string;
 | 
			
		||||
    en: string;
 | 
			
		||||
    fr: string;
 | 
			
		||||
    it: string;
 | 
			
		||||
    de: string;
 | 
			
		||||
    es: string;
 | 
			
		||||
    pt: string;
 | 
			
		||||
    ru: string;
 | 
			
		||||
    pl: string;
 | 
			
		||||
    uk: string;
 | 
			
		||||
    tr: string;
 | 
			
		||||
    ja: string;
 | 
			
		||||
    zh: string;
 | 
			
		||||
    ko: string;
 | 
			
		||||
    tc: string;
 | 
			
		||||
    th: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IFbst {
 | 
			
		||||
    a: number;
 | 
			
		||||
    e: number;
 | 
			
		||||
    n: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,603 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "VendorInfo": {
 | 
			
		||||
    "_id": {
 | 
			
		||||
      "$oid": "63ed01efbdaa38891767bac9"
 | 
			
		||||
    },
 | 
			
		||||
    "TypeName": "/Lotus/Types/Game/VendorManifests/Hubs/TeshinHardModeVendorManifest",
 | 
			
		||||
    "ItemManifest": [
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Types/Recipes/OperatorArmour/HardMode/OperatorTeshinArmsBlueprint",
 | 
			
		||||
        "ItemPrices": [
 | 
			
		||||
          {
 | 
			
		||||
            "ItemCount": 15,
 | 
			
		||||
            "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence",
 | 
			
		||||
            "ProductCategory": "MiscItems"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "Bin": "BIN_0",
 | 
			
		||||
        "QuantityMultiplier": 1,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "2051240400000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "AllowMultipurchase": true,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "66fd60b20ba592c4c95e9947"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Types/Recipes/OperatorArmour/HardMode/OperatorTeshinBodyBlueprint",
 | 
			
		||||
        "ItemPrices": [
 | 
			
		||||
          {
 | 
			
		||||
            "ItemCount": 25,
 | 
			
		||||
            "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence",
 | 
			
		||||
            "ProductCategory": "MiscItems"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "Bin": "BIN_0",
 | 
			
		||||
        "QuantityMultiplier": 1,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "2051240400000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "AllowMultipurchase": true,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "66fd60b20ba592c4c95e9948"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Types/Recipes/OperatorArmour/HardMode/OperatorTeshinHeadBlueprint",
 | 
			
		||||
        "ItemPrices": [
 | 
			
		||||
          {
 | 
			
		||||
            "ItemCount": 20,
 | 
			
		||||
            "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence",
 | 
			
		||||
            "ProductCategory": "MiscItems"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "Bin": "BIN_0",
 | 
			
		||||
        "QuantityMultiplier": 1,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "2051240400000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "AllowMultipurchase": true,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "66fd60b20ba592c4c95e9949"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Types/Recipes/OperatorArmour/HardMode/OperatorTeshinLegsBlueprint",
 | 
			
		||||
        "ItemPrices": [
 | 
			
		||||
          {
 | 
			
		||||
            "ItemCount": 25,
 | 
			
		||||
            "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence",
 | 
			
		||||
            "ProductCategory": "MiscItems"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "Bin": "BIN_0",
 | 
			
		||||
        "QuantityMultiplier": 1,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "2051240400000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "AllowMultipurchase": true,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "66fd60b20ba592c4c95e994a"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Types/Items/MiscItems/WeaponPrimaryArcaneUnlocker",
 | 
			
		||||
        "ItemPrices": [
 | 
			
		||||
          {
 | 
			
		||||
            "ItemCount": 15,
 | 
			
		||||
            "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence",
 | 
			
		||||
            "ProductCategory": "MiscItems"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "Bin": "BIN_0",
 | 
			
		||||
        "QuantityMultiplier": 1,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "2051240400000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "AllowMultipurchase": true,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "66fd60b20ba592c4c95e994b"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Types/Items/MiscItems/WeaponSecondaryArcaneUnlocker",
 | 
			
		||||
        "ItemPrices": [
 | 
			
		||||
          {
 | 
			
		||||
            "ItemCount": 15,
 | 
			
		||||
            "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence",
 | 
			
		||||
            "ProductCategory": "MiscItems"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "Bin": "BIN_0",
 | 
			
		||||
        "QuantityMultiplier": 1,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "2051240400000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "AllowMultipurchase": true,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "66fd60b20ba592c4c95e994c"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Types/Recipes/Components/FormaStanceBlueprint",
 | 
			
		||||
        "ItemPrices": [
 | 
			
		||||
          {
 | 
			
		||||
            "ItemCount": 10,
 | 
			
		||||
            "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence",
 | 
			
		||||
            "ProductCategory": "MiscItems"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "Bin": "BIN_0",
 | 
			
		||||
        "QuantityMultiplier": 1,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "2051240400000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "AllowMultipurchase": true,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "66fd60b20ba592c4c95e994d"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Upgrades/Skins/Effects/OrbsEphemera",
 | 
			
		||||
        "ItemPrices": [
 | 
			
		||||
          {
 | 
			
		||||
            "ItemCount": 3,
 | 
			
		||||
            "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence",
 | 
			
		||||
            "ProductCategory": "MiscItems"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "Bin": "BIN_0",
 | 
			
		||||
        "QuantityMultiplier": 1,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "2051240400000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "AllowMultipurchase": true,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "66fd60b20ba592c4c95e994e"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Upgrades/Skins/Effects/TatsuSkullEphemera",
 | 
			
		||||
        "ItemPrices": [
 | 
			
		||||
          {
 | 
			
		||||
            "ItemCount": 85,
 | 
			
		||||
            "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence",
 | 
			
		||||
            "ProductCategory": "MiscItems"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "Bin": "BIN_0",
 | 
			
		||||
        "QuantityMultiplier": 1,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "2051240400000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "AllowMultipurchase": true,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "66fd60b20ba592c4c95e994f"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawShotgunRandomMod",
 | 
			
		||||
        "ItemPrices": [
 | 
			
		||||
          {
 | 
			
		||||
            "ItemCount": 75,
 | 
			
		||||
            "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence",
 | 
			
		||||
            "ProductCategory": "MiscItems"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "Bin": "BIN_0",
 | 
			
		||||
        "QuantityMultiplier": 1,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "2051240400000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "PurchaseQuantityLimit": 1,
 | 
			
		||||
        "RotatedWeekly": true,
 | 
			
		||||
        "AllowMultipurchase": false,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "66fd60b20ba592c4c95e9950"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Types/Recipes/Components/UmbraFormaBlueprint",
 | 
			
		||||
        "ItemPrices": [
 | 
			
		||||
          {
 | 
			
		||||
            "ItemCount": 150,
 | 
			
		||||
            "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence",
 | 
			
		||||
            "ProductCategory": "MiscItems"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "Bin": "BIN_0",
 | 
			
		||||
        "QuantityMultiplier": 1,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "2051240400000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "PurchaseQuantityLimit": 1,
 | 
			
		||||
        "RotatedWeekly": true,
 | 
			
		||||
        "AllowMultipurchase": false,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "66fd60b20ba592c4c95e9951"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Types/Items/MiscItems/Kuva",
 | 
			
		||||
        "ItemPrices": [
 | 
			
		||||
          {
 | 
			
		||||
            "ItemCount": 55,
 | 
			
		||||
            "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence",
 | 
			
		||||
            "ProductCategory": "MiscItems"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "Bin": "BIN_0",
 | 
			
		||||
        "QuantityMultiplier": 50000,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "2051240400000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "PurchaseQuantityLimit": 1,
 | 
			
		||||
        "RotatedWeekly": true,
 | 
			
		||||
        "AllowMultipurchase": false,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "66fd60b20ba592c4c95e9952"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawModularPistolRandomMod",
 | 
			
		||||
        "ItemPrices": [
 | 
			
		||||
          {
 | 
			
		||||
            "ItemCount": 75,
 | 
			
		||||
            "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence",
 | 
			
		||||
            "ProductCategory": "MiscItems"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "Bin": "BIN_0",
 | 
			
		||||
        "QuantityMultiplier": 1,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "2051240400000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "PurchaseQuantityLimit": 1,
 | 
			
		||||
        "RotatedWeekly": true,
 | 
			
		||||
        "AllowMultipurchase": false,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "66fd60b20ba592c4c95e9953"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Types/Items/MiscItems/Forma",
 | 
			
		||||
        "ItemPrices": [
 | 
			
		||||
          {
 | 
			
		||||
            "ItemCount": 75,
 | 
			
		||||
            "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence",
 | 
			
		||||
            "ProductCategory": "MiscItems"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "Bin": "BIN_0",
 | 
			
		||||
        "QuantityMultiplier": 3,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "2051240400000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "PurchaseQuantityLimit": 1,
 | 
			
		||||
        "RotatedWeekly": true,
 | 
			
		||||
        "AllowMultipurchase": false,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "66fd60b20ba592c4c95e9954"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawModularMeleeRandomMod",
 | 
			
		||||
        "ItemPrices": [
 | 
			
		||||
          {
 | 
			
		||||
            "ItemCount": 75,
 | 
			
		||||
            "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence",
 | 
			
		||||
            "ProductCategory": "MiscItems"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "Bin": "BIN_0",
 | 
			
		||||
        "QuantityMultiplier": 1,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "2051240400000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "PurchaseQuantityLimit": 1,
 | 
			
		||||
        "RotatedWeekly": true,
 | 
			
		||||
        "AllowMultipurchase": false,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "66fd60b20ba592c4c95e9955"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Upgrades/Mods/FusionBundles/EvergreenLoginRewardFusionBundle",
 | 
			
		||||
        "ItemPrices": [
 | 
			
		||||
          {
 | 
			
		||||
            "ItemCount": 150,
 | 
			
		||||
            "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence",
 | 
			
		||||
            "ProductCategory": "MiscItems"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "Bin": "BIN_0",
 | 
			
		||||
        "QuantityMultiplier": 1,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "2051240400000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "PurchaseQuantityLimit": 1,
 | 
			
		||||
        "RotatedWeekly": true,
 | 
			
		||||
        "AllowMultipurchase": false,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "66fd60b20ba592c4c95e9956"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Upgrades/Mods/Randomized/RawRifleRandomMod",
 | 
			
		||||
        "ItemPrices": [
 | 
			
		||||
          {
 | 
			
		||||
            "ItemCount": 75,
 | 
			
		||||
            "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence",
 | 
			
		||||
            "ProductCategory": "MiscItems"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "Bin": "BIN_0",
 | 
			
		||||
        "QuantityMultiplier": 1,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "2051240400000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "PurchaseQuantityLimit": 1,
 | 
			
		||||
        "RotatedWeekly": true,
 | 
			
		||||
        "AllowMultipurchase": false,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "66fd60b20ba592c4c95e9957"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Upgrades/Mods/Shotgun/WeaponRecoilReductionMod",
 | 
			
		||||
        "ItemPrices": [
 | 
			
		||||
          {
 | 
			
		||||
            "ItemCount": 35,
 | 
			
		||||
            "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence",
 | 
			
		||||
            "ProductCategory": "MiscItems"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "Bin": "BIN_0",
 | 
			
		||||
        "QuantityMultiplier": 1,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "2051240400000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "AllowMultipurchase": true,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "66fd60b20ba592c4c95e9958"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/TeshinBobbleHead",
 | 
			
		||||
        "ItemPrices": [
 | 
			
		||||
          {
 | 
			
		||||
            "ItemCount": 35,
 | 
			
		||||
            "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence",
 | 
			
		||||
            "ProductCategory": "MiscItems"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "Bin": "BIN_0",
 | 
			
		||||
        "QuantityMultiplier": 1,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "2051240400000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "AllowMultipurchase": true,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "66fd60b20ba592c4c95e9959"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/ImageGaussVED",
 | 
			
		||||
        "ItemPrices": [
 | 
			
		||||
          {
 | 
			
		||||
            "ItemCount": 15,
 | 
			
		||||
            "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence",
 | 
			
		||||
            "ProductCategory": "MiscItems"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "Bin": "BIN_0",
 | 
			
		||||
        "QuantityMultiplier": 1,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "2051240400000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "AllowMultipurchase": true,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "66fd60b20ba592c4c95e995a"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/ImageGrendelVED",
 | 
			
		||||
        "ItemPrices": [
 | 
			
		||||
          {
 | 
			
		||||
            "ItemCount": 15,
 | 
			
		||||
            "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence",
 | 
			
		||||
            "ProductCategory": "MiscItems"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "Bin": "BIN_0",
 | 
			
		||||
        "QuantityMultiplier": 1,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "2051240400000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "AllowMultipurchase": true,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "66fd60b20ba592c4c95e995b"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageProteaAction",
 | 
			
		||||
        "ItemPrices": [
 | 
			
		||||
          {
 | 
			
		||||
            "ItemCount": 15,
 | 
			
		||||
            "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence",
 | 
			
		||||
            "ProductCategory": "MiscItems"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "Bin": "BIN_0",
 | 
			
		||||
        "QuantityMultiplier": 1,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "2051240400000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "AllowMultipurchase": true,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "66fd60b20ba592c4c95e995c"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Types/Items/ShipDecos/TeaSet",
 | 
			
		||||
        "ItemPrices": [
 | 
			
		||||
          {
 | 
			
		||||
            "ItemCount": 15,
 | 
			
		||||
            "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence",
 | 
			
		||||
            "ProductCategory": "MiscItems"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "Bin": "BIN_0",
 | 
			
		||||
        "QuantityMultiplier": 1,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "2051240400000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "AllowMultipurchase": true,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "66fd60b20ba592c4c95e995d"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Types/StoreItems/AvatarImages/AvatarImageXakuAction",
 | 
			
		||||
        "ItemPrices": [
 | 
			
		||||
          {
 | 
			
		||||
            "ItemCount": 15,
 | 
			
		||||
            "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence",
 | 
			
		||||
            "ProductCategory": "MiscItems"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "Bin": "BIN_0",
 | 
			
		||||
        "QuantityMultiplier": 1,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "2051240400000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "AllowMultipurchase": true,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "66fd60b20ba592c4c95e995e"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Types/Items/MiscItems/RivenIdentifier",
 | 
			
		||||
        "ItemPrices": [
 | 
			
		||||
          {
 | 
			
		||||
            "ItemCount": 20,
 | 
			
		||||
            "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence",
 | 
			
		||||
            "ProductCategory": "MiscItems"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "Bin": "BIN_0",
 | 
			
		||||
        "QuantityMultiplier": 1,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "2051240400000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "PurchaseQuantityLimit": 1,
 | 
			
		||||
        "RotatedWeekly": true,
 | 
			
		||||
        "AllowMultipurchase": false,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "66fd60b20ba592c4c95e995f"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Types/BoosterPacks/RandomSyndicateProjectionPack",
 | 
			
		||||
        "ItemPrices": [
 | 
			
		||||
          {
 | 
			
		||||
            "ItemCount": 15,
 | 
			
		||||
            "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence",
 | 
			
		||||
            "ProductCategory": "MiscItems"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "Bin": "BIN_0",
 | 
			
		||||
        "QuantityMultiplier": 1,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "604800000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "PurchaseQuantityLimit": 25,
 | 
			
		||||
        "AllowMultipurchase": true,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "66fd60b20ba592c4c95e997c"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "StoreItem": "/Lotus/StoreItems/Types/Items/MiscItems/Kuva",
 | 
			
		||||
        "ItemPrices": [
 | 
			
		||||
          {
 | 
			
		||||
            "ItemCount": 15,
 | 
			
		||||
            "ItemType": "/Lotus/Types/Items/MiscItems/SteelEssence",
 | 
			
		||||
            "ProductCategory": "MiscItems"
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "Bin": "BIN_0",
 | 
			
		||||
        "QuantityMultiplier": 10000,
 | 
			
		||||
        "Expiry": {
 | 
			
		||||
          "$date": {
 | 
			
		||||
            "$numberLong": "604800000"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "PurchaseQuantityLimit": 25,
 | 
			
		||||
        "AllowMultipurchase": true,
 | 
			
		||||
        "Id": {
 | 
			
		||||
          "$oid": "66fd60b20ba592c4c95e997d"
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "PropertyTextHash": "0A0F20AFA748FBEE490510DBF5A33A0D",
 | 
			
		||||
    "Expiry": {
 | 
			
		||||
      "$date": {
 | 
			
		||||
        "$numberLong": "604800000"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -3,8 +3,8 @@ dict = {
 | 
			
		||||
    general_inventoryUpdateNote: `Nota: Los cambios realizados aquí se reflejarán en el juego cuando este sincronice el inventario. Usar la navegación debería ser la forma más sencilla de activar esto.`,
 | 
			
		||||
    general_addButton: `Agregar`,
 | 
			
		||||
    general_bulkActions: `Acciones masivas`,
 | 
			
		||||
    code_loginFail: `[UNTRANSLATED] Login failed. Double-check the email and password.`,
 | 
			
		||||
    code_regFail: `[UNTRANSLATED] Registration failed. Account already exists?`,
 | 
			
		||||
    code_loginFail: `Error al iniciar sesión. Verifica el correo electrónico y la contraseña.`,
 | 
			
		||||
    code_regFail: `Error al registrar la cuenta. ¿Ya existe una cuenta con este correo?`,
 | 
			
		||||
    code_nonValidAuthz: `Tus credenciales no son válidas.`,
 | 
			
		||||
    code_changeNameConfirm: `¿Qué nombre te gustaría ponerle a tu cuenta?`,
 | 
			
		||||
    code_deleteAccountConfirm: `¿Estás seguro de que deseas eliminar tu cuenta |DISPLAYNAME| (|EMAIL|)? Esta acción es permanente.`,
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user