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