Compare commits

...

13 Commits

Author SHA1 Message Date
a27f1c5e01 fix: converting storeitems in missionRewards (#1017)
Fixes the acquisition of blueprints as rewards, such as those rewarded by the Junctions.

Reviewed-on: OpenWF/SpaceNinjaServer#1017
Co-authored-by: VampireKitten <dynamightkobold@gmail.com>
Co-committed-by: VampireKitten <dynamightkobold@gmail.com>
2025-02-25 10:08:27 -08:00
93afc2645c fix: items from enemy caches not showing "identified" (#1016)
Reviewed-on: OpenWF/SpaceNinjaServer#1016
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-25 04:42:49 -08:00
b5b088249c fix: ignore empty mission tag in missionInventoryUpdate (#1015)
Fixes #1013

Reviewed-on: OpenWF/SpaceNinjaServer#1015
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-25 04:41:45 -08:00
e6ec144f1f feat: handle defaultUpgrades for moas and hounds (#1012)
Closes #997

Reviewed-on: OpenWF/SpaceNinjaServer#1012
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-25 04:41:14 -08:00
3d82fee99e feat: give additionalItems for weapons (#1011)
Closes #1002

Reviewed-on: OpenWF/SpaceNinjaServer#1011
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-25 04:39:59 -08:00
39f0f7de9a feat: cracking relics in non-endless missions (#1010)
Closes #415

Reviewed-on: OpenWF/SpaceNinjaServer#1010
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-25 04:38:47 -08:00
f672f05db9 fix: handle bundles being given to addItems (#1005)
This is needed for the Hex noggles email attachment

Reviewed-on: OpenWF/SpaceNinjaServer#1005
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-25 04:38:17 -08:00
4d9e6a35ab fix: use correct reward manifest for arbitrations (#1004)
Closes #939

Reviewed-on: OpenWF/SpaceNinjaServer#1004
Co-authored-by: Sainan <sainan@calamity.inc>
Co-committed-by: Sainan <sainan@calamity.inc>
2025-02-25 04:36:10 -08:00
c29bf6aab5 chore: use PE+ for boosters (#1009)
Reviewed-on: OpenWF/SpaceNinjaServer#1009
2025-02-24 21:46:30 -08:00
bc07978846 chore: use creditBundles map from PE+ (#1008)
Reviewed-on: OpenWF/SpaceNinjaServer#1008
2025-02-24 21:46:20 -08:00
2efe0df2f2 chore: fix some eslint warnings (#1007)
Reviewed-on: OpenWF/SpaceNinjaServer#1007
2025-02-24 20:56:34 -08:00
38b255d41a chore: promote no-case-declarations lint to an error (#1006)
Reviewed-on: OpenWF/SpaceNinjaServer#1006
2025-02-24 20:56:27 -08:00
421164986a fix: don't throw an error if questKey already exists (#1003)
Reviewed-on: OpenWF/SpaceNinjaServer#1003
Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
2025-02-24 15:59:57 -08:00
24 changed files with 272 additions and 214 deletions

View File

@ -23,7 +23,7 @@
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-loss-of-precision": "warn",
"@typescript-eslint/no-unnecessary-condition": "warn",
"no-case-declarations": "warn",
"no-case-declarations": "error",
"prettier/prettier": "error",
"@typescript-eslint/semi": "error",
"no-mixed-spaces-and-tabs": "error",

8
package-lock.json generated
View File

@ -12,7 +12,7 @@
"copyfiles": "^2.4.1",
"express": "^5",
"mongoose": "^8.9.4",
"warframe-public-export-plus": "^0.5.35",
"warframe-public-export-plus": "^0.5.36",
"warframe-riven-info": "^0.1.2",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"
@ -4093,9 +4093,9 @@
}
},
"node_modules/warframe-public-export-plus": {
"version": "0.5.35",
"resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.35.tgz",
"integrity": "sha512-YLQP1n5sOV+PS5hfC4Kuoapa9gsqOy5Qy/E4EYfRV/xJBruFl3tPhbdbgFn3HhL2OBrgRJ8yzT5bjIvaHKhOCw=="
"version": "0.5.36",
"resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.36.tgz",
"integrity": "sha512-FYZECqBSnynl6lQvcQyEqpnGW9l84wzusekhtwKjvg3280CYdn7g5x0Q9tOMhj1jpc/1tuY+akHtHa94sPtqKw=="
},
"node_modules/warframe-riven-info": {
"version": "0.1.2",

View File

@ -17,7 +17,7 @@
"copyfiles": "^2.4.1",
"express": "^5",
"mongoose": "^8.9.4",
"warframe-public-export-plus": "^0.5.35",
"warframe-public-export-plus": "^0.5.36",
"warframe-riven-info": "^0.1.2",
"winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0"

View File

@ -1,77 +1,31 @@
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addMiscItems, getInventory } from "@/src/services/inventoryService";
import { crackRelic } from "@/src/helpers/relicHelper";
import { getInventory } from "@/src/services/inventoryService";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
import { getRandomWeightedReward2 } from "@/src/services/rngService";
import { ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger";
import { IVoidTearParticipantInfo } from "@/src/types/requestTypes";
import { RequestHandler } from "express";
import { ExportRelics, ExportRewards, TRarity } from "warframe-public-export-plus";
export const getVoidProjectionRewardsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
const data = getJSONfromString<IVoidProjectionRewardRequest>(String(req.body));
if (data.ParticipantInfo.QualifiesForReward && !data.ParticipantInfo.HaveRewardResponse) {
const inventory = await getInventory(accountId);
await crackRelic(inventory, data.ParticipantInfo);
await inventory.save();
}
const response: IVoidProjectionRewardResponse = {
CurrentWave: data.CurrentWave,
ParticipantInfo: data.ParticipantInfo,
DifficultyTier: data.DifficultyTier
};
if (data.ParticipantInfo.QualifiesForReward) {
const relic = ExportRelics[data.ParticipantInfo.VoidProjection];
const weights = refinementToWeights[relic.quality];
logger.debug(`opening a relic of quality ${relic.quality}; rarity weights are`, weights);
const reward = getRandomWeightedReward2(
ExportRewards[relic.rewardManifest][0] as { type: string; itemCount: number; rarity: TRarity }[], // rarity is nullable in PE+ typings, but always present for relics
weights
)!;
logger.debug(`relic rolled`, reward);
response.ParticipantInfo.Reward = reward.type;
const inventory = await getInventory(accountId);
// Remove relic
addMiscItems(inventory, [
{
ItemType: data.ParticipantInfo.VoidProjection,
ItemCount: -1
}
]);
// Give reward
await handleStoreItemAcquisition(reward.type, inventory, reward.itemCount);
await inventory.save();
}
res.json(response);
};
const refinementToWeights = {
VPQ_BRONZE: {
COMMON: 0.76,
UNCOMMON: 0.22,
RARE: 0.02,
LEGENDARY: 0
},
VPQ_SILVER: {
COMMON: 0.7,
UNCOMMON: 0.26,
RARE: 0.04,
LEGENDARY: 0
},
VPQ_GOLD: {
COMMON: 0.6,
UNCOMMON: 0.34,
RARE: 0.06,
LEGENDARY: 0
},
VPQ_PLATINUM: {
COMMON: 0.5,
UNCOMMON: 0.4,
RARE: 0.1,
LEGENDARY: 0
}
};
interface IVoidProjectionRewardRequest {
CurrentWave: number;
ParticipantInfo: IParticipantInfo;
ParticipantInfo: IVoidTearParticipantInfo;
VoidTier: string;
DifficultyTier: number;
VoidProjectionRemovalHash: string;
@ -79,20 +33,6 @@ interface IVoidProjectionRewardRequest {
interface IVoidProjectionRewardResponse {
CurrentWave: number;
ParticipantInfo: IParticipantInfo;
ParticipantInfo: IVoidTearParticipantInfo;
DifficultyTier: number;
}
interface IParticipantInfo {
AccountId: string;
Name: string;
ChosenRewardOwner: string;
MissionHash: string;
VoidProjection: string;
Reward: string;
QualifiesForReward: boolean;
HaveRewardResponse: boolean;
RewardsMultiplier: number;
RewardProjection: string;
HardModeReward: ITypeCount;
}

View File

@ -39,7 +39,7 @@ const awakeningRewards = [
export const addStartingGear = async (
inventory: HydratedDocument<IInventoryDatabase, InventoryDocumentProps>,
startingGear: TPartialStartingGear | undefined = undefined
) => {
): Promise<IInventoryChanges> => {
const { LongGuns, Pistols, Suits, Melee } = startingGear || {
LongGuns: [{ ItemType: "/Lotus/Weapons/Tenno/Rifle/Rifle" }],
Pistols: [{ ItemType: "/Lotus/Weapons/Tenno/Pistol/Pistol" }],

View File

@ -249,7 +249,7 @@ export const infestedFoundryController: RequestHandler = async (req, res) => {
const suit = inventory.Suits.id(request.SuitId.$oid)!;
inventory.Suits.pull(suit);
const consumedSuit: IConsumedSuit = { s: suit.ItemType };
if (suit.Configs && suit.Configs[0] && suit.Configs[0].pricol) {
if (suit.Configs[0] && suit.Configs[0].pricol) {
consumedSuit.c = suit.Configs[0].pricol;
}
if ((inventory.InfestedFoundry!.XP ?? 0) < 73125_00) {

View File

@ -2,7 +2,14 @@ import { RequestHandler } from "express";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes";
import { getInventory, updateCurrency, addEquipment, addMiscItems } from "@/src/services/inventoryService";
import {
getInventory,
updateCurrency,
addEquipment,
addMiscItems,
applyDefaultUpgrades
} from "@/src/services/inventoryService";
import { ExportWeapons } from "warframe-public-export-plus";
const modularWeaponTypes: Record<string, TEquipmentKey> = {
"/Lotus/Weapons/SolarisUnited/Primary/LotusModularPrimary": "LongGuns",
@ -36,8 +43,11 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res)
const category = modularWeaponTypes[data.WeaponType];
const inventory = await getInventory(accountId);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const configs = applyDefaultUpgrades(inventory, ExportWeapons[data.Parts[0]]?.defaultUpgrades);
// Give weapon
const weapon = addEquipment(inventory, category, data.WeaponType, data.Parts);
const inventoryChanges = addEquipment(inventory, category, data.WeaponType, data.Parts, {}, { Configs: configs });
// Remove credits & parts
const miscItemChanges = [];
@ -58,8 +68,8 @@ export const modularWeaponCraftingController: RequestHandler = async (req, res)
// Tell client what we did
res.json({
InventoryChanges: {
...inventoryChanges,
...currencyChanges,
[category]: [weapon],
MiscItems: miscItemChanges
}
});

View File

@ -50,6 +50,7 @@ const qualityKeywordToNumber: Record<VoidProjectionQuality, number> = {
// e.g. "/Lotus/Types/Game/Projections/T2VoidProjectionProteaPrimeDBronze" -> ["Lith", "W5", "VPQ_BRONZE"]
const parseProjection = (typeName: string): [string, string, VoidProjectionQuality] => {
const relic: IRelic | undefined = ExportRelics[typeName];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!relic) {
throw new Error(`Unknown projection ${typeName}`);
}

View File

@ -39,17 +39,11 @@ const getItemListsController: RequestHandler = (req, response) => {
res.miscitems = [];
res.Syndicates = [];
for (const [uniqueName, item] of Object.entries(ExportWarframes)) {
if (
item.productCategory == "Suits" ||
item.productCategory == "SpaceSuits" ||
item.productCategory == "MechSuits"
) {
res[item.productCategory].push({
uniqueName,
name: getString(item.name, lang),
exalted: item.exalted
});
}
res[item.productCategory].push({
uniqueName,
name: getString(item.name, lang),
exalted: item.exalted
});
}
for (const [uniqueName, item] of Object.entries(ExportSentinels)) {
if (item.productCategory == "Sentinels") {

View File

@ -0,0 +1,60 @@
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import { IVoidTearParticipantInfo } from "@/src/types/requestTypes";
import { ExportRelics, ExportRewards, TRarity } from "warframe-public-export-plus";
import { getRandomWeightedReward2 } from "@/src/services/rngService";
import { logger } from "@/src/utils/logger";
import { addMiscItems } from "@/src/services/inventoryService";
import { handleStoreItemAcquisition } from "@/src/services/purchaseService";
export const crackRelic = async (
inventory: TInventoryDatabaseDocument,
participant: IVoidTearParticipantInfo
): Promise<void> => {
const relic = ExportRelics[participant.VoidProjection];
const weights = refinementToWeights[relic.quality];
logger.debug(`opening a relic of quality ${relic.quality}; rarity weights are`, weights);
const reward = getRandomWeightedReward2(
ExportRewards[relic.rewardManifest][0] as { type: string; itemCount: number; rarity: TRarity }[], // rarity is nullable in PE+ typings, but always present for relics
weights
)!;
logger.debug(`relic rolled`, reward);
participant.Reward = reward.type;
// Remove relic
addMiscItems(inventory, [
{
ItemType: participant.VoidProjection,
ItemCount: -1
}
]);
// Give reward
await handleStoreItemAcquisition(reward.type, inventory, reward.itemCount);
};
const refinementToWeights = {
VPQ_BRONZE: {
COMMON: 0.76,
UNCOMMON: 0.22,
RARE: 0.02,
LEGENDARY: 0
},
VPQ_SILVER: {
COMMON: 0.7,
UNCOMMON: 0.26,
RARE: 0.04,
LEGENDARY: 0
},
VPQ_GOLD: {
COMMON: 0.6,
UNCOMMON: 0.34,
RARE: 0.06,
LEGENDARY: 0
},
VPQ_PLATINUM: {
COMMON: 0.5,
UNCOMMON: 0.4,
RARE: 0.1,
LEGENDARY: 0
}
};

View File

@ -25,8 +25,10 @@ mongoose
cert: fs.readFileSync("static/certs/cert.pem")
};
// eslint-disable-next-line @typescript-eslint/no-misused-promises
http.createServer(app).listen(httpPort, () => {
logger.info("HTTP server started on port " + httpPort);
// eslint-disable-next-line @typescript-eslint/no-misused-promises
https.createServer(options, app).listen(httpsPort, () => {
logger.info("HTTPS server started on port " + httpsPort);

View File

@ -808,7 +808,7 @@ detailsSchema.set("toJSON", {
const EquipmentSchema = new Schema<IEquipmentDatabase>(
{
ItemType: String,
Configs: [ItemConfigSchema],
Configs: { type: [ItemConfigSchema], default: [] },
UpgradeVer: { type: Number, default: 101 },
XP: { type: Number, default: 0 },
Features: Number,
@ -1303,7 +1303,7 @@ inventorySchema.set("toJSON", {
if (inventoryDatabase.GuildId) {
inventoryResponse.GuildId = toOid(inventoryDatabase.GuildId);
}
if (inventoryResponse.BlessingCooldown) {
if (inventoryDatabase.BlessingCooldown) {
inventoryResponse.BlessingCooldown = toMongoDate(inventoryDatabase.BlessingCooldown);
}
}

View File

@ -50,6 +50,7 @@ const convertEquipment = (client: IEquipmentClient): IEquipmentDatabase => {
UpgradesExpiry: convertOptionalDate(client.UpgradesExpiry),
CrewMembers: client.CrewMembers ? convertCrewShipMembers(client.CrewMembers) : undefined,
Details: client.Details ? convertKubrowDetails(client.Details) : undefined,
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
Configs: client.Configs
? client.Configs.map(obj =>
Object.fromEntries(

View File

@ -27,7 +27,7 @@ export const deleteAllMessagesRead = async (accountId: string): Promise<void> =>
await Inbox.deleteMany({ ownerId: accountId, r: true });
};
export const createNewEventMessages = async (req: Request) => {
export const createNewEventMessages = async (req: Request): Promise<void> => {
const account = await getAccountForRequest(req);
const latestEventMessageDate = account.LatestEventMessageDate;

View File

@ -38,27 +38,30 @@ import { getExalted, getKeyChainItems } from "@/src/services/itemDataService";
import { IEquipmentClient, IEquipmentDatabase, IItemConfig } from "../types/inventoryTypes/commonInventoryTypes";
import {
ExportArcanes,
ExportBundles,
ExportCustoms,
ExportDrones,
ExportFlavour,
ExportFusionBundles,
ExportGear,
ExportKeys,
ExportMisc,
ExportRecipes,
ExportResources,
ExportSentinels,
ExportSyndicates,
ExportUpgrades,
ExportWeapons,
IDefaultUpgrade,
TStandingLimitBin
} from "warframe-public-export-plus";
import { createShip } from "./shipService";
import { creditBundles } from "@/src/services/missionInventoryUpdateService";
import { IKeyChainRequest } from "@/src/controllers/api/giveKeyChainTriggeredItemsController";
import { toOid } from "../helpers/inventoryHelpers";
import { generateRewardSeed } from "@/src/controllers/api/getNewRewardSeedController";
import { addStartingGear } from "@/src/controllers/api/giveStartingGearController";
import { addQuestKey, completeQuest } from "@/src/services/questService";
import { handleBundleAcqusition } from "./purchaseService";
export const createInventory = async (
accountOwnerId: Types.ObjectId,
@ -157,6 +160,11 @@ export const addItem = async (
typeName: string,
quantity: number = 1
): Promise<{ InventoryChanges: IInventoryChanges }> => {
// Bundles are technically StoreItems but a) they don't have a normal counterpart, and b) they are used in non-StoreItem contexts, e.g. email attachments.
if (typeName in ExportBundles) {
return { InventoryChanges: await handleBundleAcqusition(typeName, inventory, quantity) };
}
// Strict typing
if (typeName in ExportRecipes) {
const recipeChanges = [
@ -203,6 +211,7 @@ export const addItem = async (
const inventoryChanges = {
...addCrewShip(inventory, typeName),
// fix to unlock railjack modding, item bellow supposed to be obtained from archwing quest
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
...(!inventory.CrewShipHarnesses?.length
? addCrewShipHarness(inventory, "/Lotus/Types/Game/CrewShip/RailJack/DefaultHarness")
: {})
@ -284,6 +293,11 @@ export const addItem = async (
const weapon = ExportWeapons[typeName];
if (weapon.totalDamage != 0) {
const inventoryChanges = addEquipment(inventory, weapon.productCategory, typeName);
if (weapon.additionalItems) {
for (const item of weapon.additionalItems) {
combineInventoryChanges(inventoryChanges, await addItem(inventory, item, 1));
}
}
updateSlots(inventory, InventorySlot.WEAPONS, 0, 1);
return {
InventoryChanges: {
@ -307,8 +321,8 @@ export const addItem = async (
};
}
}
if (typeName in creditBundles) {
const creditsTotal = creditBundles[typeName] * quantity;
if (typeName in ExportMisc.creditBundles) {
const creditsTotal = ExportMisc.creditBundles[typeName] * quantity;
inventory.RegularCredits += creditsTotal;
return {
InventoryChanges: {
@ -331,9 +345,8 @@ export const addItem = async (
if (key.chainStages) {
const key = addQuestKey(inventory, { ItemType: typeName });
if (key) {
return { InventoryChanges: { QuestKeys: [key] } };
}
if (!key) return { InventoryChanges: {} };
return { InventoryChanges: { QuestKeys: [key] } };
} else {
const key = { ItemType: typeName, ItemCount: quantity };
@ -405,18 +418,21 @@ export const addItem = async (
switch (typeName.substr(1).split("/")[2]) {
case "Mods": // Legendary Core
case "CosmeticEnhancers": // Traumatic Peculiar
const changes = [
{
ItemType: typeName,
ItemCount: quantity
}
];
addMods(inventory, changes);
return {
InventoryChanges: {
RawUpgrades: changes
}
};
{
const changes = [
{
ItemType: typeName,
ItemCount: quantity
}
];
addMods(inventory, changes);
return {
InventoryChanges: {
RawUpgrades: changes
}
};
}
break;
}
break;
}
@ -517,21 +533,15 @@ export const addItems = async (
return inventoryChanges;
};
//TODO: maybe genericMethod for all the add methods, they share a lot of logic
export const addSentinel = (
export const applyDefaultUpgrades = (
inventory: TInventoryDatabaseDocument,
sentinelName: string,
inventoryChanges: IInventoryChanges = {}
): IInventoryChanges => {
if (ExportSentinels[sentinelName]?.defaultWeapon) {
addSentinelWeapon(inventory, ExportSentinels[sentinelName].defaultWeapon, inventoryChanges);
}
defaultUpgrades: IDefaultUpgrade[] | undefined
): IItemConfig[] => {
const modsToGive: IRawUpgrade[] = [];
const configs: IItemConfig[] = [];
if (ExportSentinels[sentinelName]?.defaultUpgrades) {
if (defaultUpgrades) {
const upgrades = [];
for (const defaultUpgrade of ExportSentinels[sentinelName].defaultUpgrades) {
for (const defaultUpgrade of defaultUpgrades) {
modsToGive.push({ ItemType: defaultUpgrade.ItemType, ItemCount: 1 });
if (defaultUpgrade.Slot != -1) {
upgrades[defaultUpgrade.Slot] = defaultUpgrade.ItemType;
@ -541,8 +551,24 @@ export const addSentinel = (
configs.push({ Upgrades: upgrades });
}
}
addMods(inventory, modsToGive);
return configs;
};
//TODO: maybe genericMethod for all the add methods, they share a lot of logic
export const addSentinel = (
inventory: TInventoryDatabaseDocument,
sentinelName: string,
inventoryChanges: IInventoryChanges = {}
): IInventoryChanges => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (ExportSentinels[sentinelName]?.defaultWeapon) {
addSentinelWeapon(inventory, ExportSentinels[sentinelName].defaultWeapon, inventoryChanges);
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const configs: IItemConfig[] = applyDefaultUpgrades(inventory, ExportSentinels[sentinelName]?.defaultUpgrades);
const sentinelIndex = inventory.Sentinels.push({ ItemType: sentinelName, Configs: configs, XP: 0 }) - 1;
inventoryChanges.Sentinels ??= [];
inventoryChanges.Sentinels.push(inventory.Sentinels[sentinelIndex].toJSON<IEquipmentClient>());

View File

@ -33,6 +33,7 @@ import { getEntriesUnsafe } from "@/src/utils/ts-utils";
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { handleStoreItemAcquisition } from "./purchaseService";
import { IMissionReward } from "../types/missionTypes";
import { crackRelic } from "@/src/helpers/relicHelper";
const getRotations = (rotationCount: number): number[] => {
if (rotationCount === 0) return [0];
@ -51,27 +52,6 @@ const getRandomRewardByChance = (pool: IReward[]): IRngResult | undefined => {
return getRandomReward(pool as IRngResult[]);
};
export const creditBundles: Record<string, number> = {
"/Lotus/Types/PickUps/Credits/1500Credits": 1500,
"/Lotus/Types/PickUps/Credits/2000Credits": 2000,
"/Lotus/Types/PickUps/Credits/2500Credits": 2500,
"/Lotus/Types/PickUps/Credits/3000Credits": 3000,
"/Lotus/Types/PickUps/Credits/4000Credits": 4000,
"/Lotus/Types/PickUps/Credits/5000Credits": 5000,
"/Lotus/Types/PickUps/Credits/7500Credits": 7500,
"/Lotus/Types/PickUps/Credits/10000Credits": 10000,
"/Lotus/Types/PickUps/Credits/5000Hollars": 5000,
"/Lotus/Types/PickUps/Credits/7500Hollars": 7500,
"/Lotus/Types/PickUps/Credits/10000Hollars": 10000,
"/Lotus/Types/PickUps/Credits/CorpusArenaCreditRewards/CorpusArenaRewardOneHard": 105000,
"/Lotus/Types/PickUps/Credits/CorpusArenaCreditRewards/CorpusArenaRewardTwoHard": 175000,
"/Lotus/Types/PickUps/Credits/CorpusArenaCreditRewards/CorpusArenaRewardThreeHard": 250000,
"/Lotus/Types/StoreItems/CreditBundles/Zariman/TableACreditsCommon": 15000,
"/Lotus/Types/StoreItems/CreditBundles/Zariman/TableACreditsUncommon": 30000,
"/Lotus/Types/StoreItems/CreditBundles/CreditBundleA": 50000,
"/Lotus/Types/StoreItems/CreditBundles/CreditBundleC": 175000
};
//type TMissionInventoryUpdateKeys = keyof IMissionInventoryUpdateRequest;
//const ignoredInventoryUpdateKeys = ["FpsAvg", "FpsMax", "FpsMin", "FpsSamples"] satisfies TMissionInventoryUpdateKeys[]; // for keys with no meaning for this server
//type TignoredInventoryUpdateKeys = (typeof ignoredInventoryUpdateKeys)[number];
@ -80,7 +60,7 @@ export const creditBundles: Record<string, number> = {
export const addMissionInventoryUpdates = (
inventory: HydratedDocument<IInventoryDatabase, InventoryDocumentProps>,
inventoryUpdates: IMissionInventoryUpdateRequest
) => {
): Partial<IInventoryDatabase> | undefined => {
//TODO: type this properly
const inventoryChanges: Partial<IInventoryDatabase> = {};
if (inventoryUpdates.MissionFailed === true) {
@ -208,14 +188,14 @@ export const addMissionInventoryUpdates = (
inventory.CompletedSorties.push(value);
break;
}
case "SeasonChallengeCompletions":
case "SeasonChallengeCompletions": {
const processedCompletions = value.map(({ challenge, id }) => ({
challenge: challenge.substring(challenge.lastIndexOf("/") + 1),
id
}));
inventory.SeasonChallengeHistory.push(...processedCompletions);
break;
}
default:
// Equipment XP updates
if (equipmentKeys.includes(key as TEquipmentKey)) {
@ -242,7 +222,8 @@ export const addMissionRewards = async (
RewardInfo: rewardInfo,
LevelKeyName: levelKeyName,
Missions: missions,
RegularCredits: creditDrops
RegularCredits: creditDrops,
VoidTearParticipantsCurrWave: voidTearWave
}: IMissionInventoryUpdateRequest
) => {
if (!rewardInfo) {
@ -252,9 +233,7 @@ export const addMissionRewards = async (
}
//TODO: check double reward merging
const MissionRewards = getRandomMissionDrops(rewardInfo).map(drop => {
return { StoreItem: drop.type, ItemCount: drop.itemCount };
});
const MissionRewards: IMissionReward[] = getRandomMissionDrops(rewardInfo);
logger.debug("random mission drops:", MissionRewards);
const inventoryChanges: IInventoryChanges = {};
@ -282,12 +261,15 @@ export const addMissionRewards = async (
}
}
if (missions) {
if (
missions &&
missions.Tag != "" // #1013
) {
const node = getNode(missions.Tag);
//node based credit rewards for mission completion
if (node.missionIndex !== 28) {
const levelCreditReward = getLevelCreditRewards(missions?.Tag);
const levelCreditReward = getLevelCreditRewards(missions.Tag);
missionCompletionCredits += levelCreditReward;
inventory.RegularCredits += levelCreditReward;
logger.debug(`levelCreditReward ${levelCreditReward}`);
@ -312,6 +294,15 @@ export const addMissionRewards = async (
rngRewardCredits: inventoryChanges.RegularCredits ?? 0
});
if (
voidTearWave &&
voidTearWave.Participants[0].QualifiesForReward &&
!voidTearWave.Participants[0].HaveRewardResponse
) {
await crackRelic(inventory, voidTearWave.Participants[0]);
MissionRewards.push({ StoreItem: voidTearWave.Participants[0].Reward, ItemCount: 1 });
}
return { inventoryChanges, MissionRewards, credits };
};
@ -360,7 +351,7 @@ export const addFixedLevelRewards = (
if (rewards.items) {
for (const item of rewards.items) {
MissionRewards.push({
StoreItem: `/Lotus/StoreItems${item.substring("Lotus/".length)}`,
StoreItem: item.includes(`/StoreItems/`) ? item : `/Lotus/StoreItems${item.substring("Lotus/".length)}`,
ItemCount: 1
});
}
@ -368,7 +359,9 @@ export const addFixedLevelRewards = (
if (rewards.countedItems) {
for (const item of rewards.countedItems) {
MissionRewards.push({
StoreItem: `/Lotus/StoreItems${item.ItemType.substring("Lotus/".length)}`,
StoreItem: item.ItemType.includes(`/StoreItems/`)
? item.ItemType
: `/Lotus/StoreItems${item.ItemType.substring("Lotus/".length)}`,
ItemCount: item.ItemCount
});
}
@ -389,11 +382,14 @@ function getLevelCreditRewards(nodeName: string): number {
//TODO: get dark sektor fixed credit rewards and railjack bonus
}
function getRandomMissionDrops(RewardInfo: IRewardInfo): IRngResult[] {
const drops: IRngResult[] = [];
function getRandomMissionDrops(RewardInfo: IRewardInfo): IMissionReward[] {
const drops: IMissionReward[] = [];
if (RewardInfo.node in ExportRegions) {
const region = ExportRegions[RewardInfo.node];
const rewardManifests = region.rewardManifests ?? [];
const rewardManifests: string[] =
RewardInfo.periodicMissionTag == "EliteAlert" || RewardInfo.periodicMissionTag == "EliteAlertB"
? ["/Lotus/Types/Game/MissionDecks/EliteAlertMissionRewards/EliteAlertMissionRewards"]
: region.rewardManifests;
let rotations: number[] = [];
if (RewardInfo.VaultsCracked) {
@ -412,7 +408,7 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo): IRngResult[] {
const rotationRewards = table[rotation];
const drop = getRandomRewardByChance(rotationRewards);
if (drop) {
drops.push(drop);
drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount });
}
}
});
@ -422,7 +418,7 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo): IRngResult[] {
for (let rotation = 0; rotation != RewardInfo.EnemyCachesFound; ++rotation) {
const drop = getRandomRewardByChance(deck[rotation]);
if (drop) {
drops.push(drop);
drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount, FromEnemyCache: true });
}
}
}
@ -441,7 +437,7 @@ function getRandomMissionDrops(RewardInfo: IRewardInfo): IRngResult[] {
const drop = getRandomRewardByChance(deck[rotation]);
if (drop) {
drops.push(drop);
drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount });
}
}
}

View File

@ -16,8 +16,10 @@ import { logger } from "@/src/utils/logger";
import worldState from "@/static/fixed_responses/worldState/worldState.json";
import {
ExportBoosterPacks,
ExportBoosters,
ExportBundles,
ExportGear,
ExportMisc,
ExportResources,
ExportSyndicates,
ExportVendors,
@ -25,7 +27,6 @@ import {
} from "warframe-public-export-plus";
import { config } from "./configService";
import { TInventoryDatabaseDocument } from "../models/inventoryModels/inventoryModel";
import { creditBundles } from "./missionInventoryUpdateService";
export const getStoreItemCategory = (storeItem: string): string => {
const storeItemString = getSubstringFromKeyword(storeItem, "StoreItems/");
@ -199,6 +200,31 @@ const handleItemPrices = (
}
};
export const handleBundleAcqusition = async (
storeItemName: string,
inventory: TInventoryDatabaseDocument,
quantity: number = 1,
inventoryChanges: IInventoryChanges = {}
): Promise<IInventoryChanges> => {
const bundle = ExportBundles[storeItemName];
logger.debug("acquiring bundle", bundle);
for (const component of bundle.components) {
combineInventoryChanges(
inventoryChanges,
(
await handleStoreItemAcquisition(
component.typeName,
inventory,
component.purchaseQuantity * quantity,
component.durability,
true
)
).InventoryChanges
);
}
return inventoryChanges;
};
export const handleStoreItemAcquisition = async (
storeItemName: string,
inventory: TInventoryDatabaseDocument,
@ -211,22 +237,7 @@ export const handleStoreItemAcquisition = async (
};
logger.debug(`handling acquision of ${storeItemName}`);
if (storeItemName in ExportBundles) {
const bundle = ExportBundles[storeItemName];
logger.debug("acquiring bundle", bundle);
for (const component of bundle.components) {
combineInventoryChanges(
purchaseResponse.InventoryChanges,
(
await handleStoreItemAcquisition(
component.typeName,
inventory,
component.purchaseQuantity * quantity,
component.durability,
true
)
).InventoryChanges
);
}
await handleBundleAcqusition(storeItemName, inventory, quantity, purchaseResponse.InventoryChanges);
} else {
const storeCategory = getStoreItemCategory(storeItemName);
const internalName = storeItemName.replace("/StoreItems", "");
@ -247,7 +258,7 @@ export const handleStoreItemAcquisition = async (
purchaseResponse = await handleTypesPurchase(internalName, inventory, quantity);
break;
case "Boosters":
purchaseResponse = handleBoostersPurchase(internalName, inventory, durability);
purchaseResponse = handleBoostersPurchase(storeItemName, inventory, durability);
break;
}
}
@ -335,8 +346,8 @@ const handleCreditBundlePurchase = async (
typeName: string,
inventory: TInventoryDatabaseDocument
): Promise<IPurchaseResponse> => {
if (typeName && typeName in creditBundles) {
const creditsAmount = creditBundles[typeName];
if (typeName && typeName in ExportMisc.creditBundles) {
const creditsAmount = ExportMisc.creditBundles[typeName];
inventory.RegularCredits += creditsAmount;
await inventory.save();
@ -367,32 +378,18 @@ const handleTypesPurchase = async (
}
};
const boosterCollection = [
"/Lotus/Types/Boosters/ResourceAmountBooster",
"/Lotus/Types/Boosters/AffinityBooster",
"/Lotus/Types/Boosters/ResourceDropChanceBooster",
"/Lotus/Types/Boosters/CreditBooster"
];
const boosterDuration: Record<TRarity, number> = {
COMMON: 3 * 86400,
UNCOMMON: 7 * 86400,
RARE: 30 * 86400,
LEGENDARY: 90 * 86400
};
const handleBoostersPurchase = (
boosterStoreName: string,
inventory: TInventoryDatabaseDocument,
durability: TRarity
): { InventoryChanges: IInventoryChanges } => {
const ItemType = boosterStoreName.replace("StoreItem", "");
if (!boosterCollection.find(x => x == ItemType)) {
logger.error(`unknown booster type: ${ItemType}`);
if (!(boosterStoreName in ExportBoosters)) {
logger.error(`unknown booster type: ${boosterStoreName}`);
return { InventoryChanges: {} };
}
const ExpiryDate = boosterDuration[durability];
const ItemType = ExportBoosters[boosterStoreName].typeName;
const ExpiryDate = ExportMisc.boosterDurations[durability];
addBooster(ItemType, ExpiryDate, inventory);

View File

@ -15,6 +15,7 @@ import { logger } from "@/src/utils/logger";
import { HydratedDocument } from "mongoose";
import { ExportKeys } from "warframe-public-export-plus";
import { addFixedLevelRewards } from "./missionInventoryUpdateService";
import { IInventoryChanges } from "../types/purchaseTypes";
export interface IUpdateQuestRequest {
QuestKeys: Omit<IQuestKeyDatabase, "CompletionDate">[];
@ -64,6 +65,7 @@ export const updateQuestStage = (
const questStage = quest.Progress[ChainStage];
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!questStage) {
const questStageIndex = quest.Progress.push(questStageUpdate) - 1;
if (questStageIndex !== ChainStage) {
@ -77,7 +79,7 @@ export const updateQuestStage = (
export const addQuestKey = (inventory: TInventoryDatabaseDocument, questKey: IQuestKeyDatabase) => {
if (inventory.QuestKeys.some(q => q.ItemType === questKey.ItemType)) {
logger.error(`quest key ${questKey.ItemType} already exists`);
logger.warn(`Quest key ${questKey.ItemType} already exists. It will not be added`);
return;
}
const index = inventory.QuestKeys.push(questKey);
@ -86,6 +88,7 @@ export const addQuestKey = (inventory: TInventoryDatabaseDocument, questKey: IQu
};
export const completeQuest = async (inventory: TInventoryDatabaseDocument, questKey: string) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const chainStages = ExportKeys[questKey]?.chainStages;
if (!chainStages) {
@ -164,7 +167,10 @@ export const completeQuest = async (inventory: TInventoryDatabaseDocument, quest
//TODO: handle quest completion items
};
export const giveKeyChainItem = async (inventory: TInventoryDatabaseDocument, keyChainInfo: IKeyChainRequest) => {
export const giveKeyChainItem = async (
inventory: TInventoryDatabaseDocument,
keyChainInfo: IKeyChainRequest
): Promise<IInventoryChanges> => {
const inventoryChanges = await addKeyChainItems(inventory, keyChainInfo);
if (isEmptyObject(inventoryChanges)) {
@ -189,7 +195,7 @@ export const giveKeyChainMessage = async (
inventory: TInventoryDatabaseDocument,
accountId: string,
keyChainInfo: IKeyChainRequest
) => {
): Promise<void> => {
const keyChainMessage = getKeyChainMessage(keyChainInfo);
const message = {

View File

@ -148,7 +148,7 @@ export const handleInventoryItemConfigChange = async (
const itemEntries = equipment as IItemEntry;
for (const [itemId, itemConfigEntries] of Object.entries(itemEntries)) {
const inventoryItem = inventory[equipmentName].find(item => item._id?.toString() === itemId);
const inventoryItem = inventory[equipmentName].id(itemId);
if (!inventoryItem) {
throw new Error(`inventory item ${equipmentName} not found with id ${itemId}`);

View File

@ -58,6 +58,7 @@ export const updateStats = async (playerStats: TStatsDatabaseDocument, payload:
break;
default:
if (!ignoredCategories.includes(category)) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!unknownCategories[action]) {
unknownCategories[action] = [];
}
@ -105,7 +106,7 @@ export const updateStats = async (playerStats: TStatsDatabaseDocument, payload:
case "FIRE_WEAPON":
case "HIT_ENTITY_ITEM":
case "HEADSHOT_ITEM":
case "KILL_ENEMY_ITEM":
case "KILL_ENEMY_ITEM": {
playerStats.Weapons ??= [];
const statKey = {
FIRE_WEAPON: "fired",
@ -126,10 +127,11 @@ export const updateStats = async (playerStats: TStatsDatabaseDocument, payload:
}
}
break;
}
case "KILL_ENEMY":
case "EXECUTE_ENEMY":
case "HEADSHOT":
case "HEADSHOT": {
playerStats.Enemies ??= [];
const enemyStatKey = {
KILL_ENEMY: "kills",
@ -149,6 +151,7 @@ export const updateStats = async (playerStats: TStatsDatabaseDocument, payload:
}
}
break;
}
case "DIE":
playerStats.Enemies ??= [];
@ -229,6 +232,7 @@ export const updateStats = async (playerStats: TStatsDatabaseDocument, payload:
default:
if (!ignoredCategories.includes(category)) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!unknownCategories[action]) {
unknownCategories[action] = [];
}

View File

@ -51,7 +51,7 @@ export interface IInventoryDatabase
GuildId?: Types.ObjectId;
PendingRecipes: IPendingRecipe[];
QuestKeys: IQuestKeyDatabase[];
BlessingCooldown: Date;
BlessingCooldown?: Date;
Ships: Types.ObjectId[];
WeaponSkins: IWeaponSkinDatabase[];
Upgrades: IUpgradeDatabase[];
@ -300,7 +300,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
PlayedParkourTutorial: boolean;
SubscribedToEmailsPersonalized: number;
InfestedFoundry?: IInfestedFoundryClient;
BlessingCooldown: IMongoDate;
BlessingCooldown?: IMongoDate;
CrewShipRawSalvage: IConsumable[];
CrewMembers: ICrewMember[];
LotusCustomization: ILotusCustomization;
@ -309,7 +309,7 @@ export interface IInventoryClient extends IDailyAffiliations, InventoryClientEqu
LastInventorySync: IOid;
NextRefill: IMongoDate; // Next time argon crystals will have a decay tick
FoundToday?: IMiscItem[]; // for Argon Crystals
CustomMarkers: ICustomMarkers[];
CustomMarkers?: ICustomMarkers[];
ActiveLandscapeTraps: any[];
EvolutionProgress?: IEvolutionProgress[];
RepVotes: any[];

View File

@ -8,4 +8,6 @@ export interface IMissionReward {
ItemCount: number;
TweetText?: string;
ProductCategory?: string;
FromEnemyCache?: boolean;
IsStrippedItem?: boolean;
}

View File

@ -87,6 +87,11 @@ export type IMissionInventoryUpdateRequest = {
PlayerSkillGains: IPlayerSkills;
CustomMarkers?: ICustomMarkers[];
LoreFragmentScans?: ILoreFragmentScan[];
VoidTearParticipantsCurrWave?: {
Wave: number;
IsFinalWave: boolean;
Participants: IVoidTearParticipantInfo[];
};
} & {
[K in TEquipmentKey]?: IEquipmentClient[];
};
@ -136,3 +141,17 @@ export interface IUnlockShipFeatureRequest {
KeyChain: string;
ChainStage: number;
}
export interface IVoidTearParticipantInfo {
AccountId: string;
Name: string;
ChosenRewardOwner: string;
MissionHash: string;
VoidProjection: string;
Reward: string;
QualifiesForReward: boolean;
HaveRewardResponse: boolean;
RewardsMultiplier: number;
RewardProjection: string;
HardModeReward: ITypeCount;
}

View File

@ -33,9 +33,9 @@ const consolelogFormat = format.printf(info => {
colors: true
});
return `${info.timestamp} [${info.version}] ${info.level}: ${info.message} ${metadataString}`;
return `${info.timestamp as string} [${info.version as string}] ${info.level}: ${info.message as string} ${metadataString}`;
}
return `${info.timestamp} [${info.version}] ${info.level}: ${info.message}`;
return `${info.timestamp as string} [${info.version as string}] ${info.level}: ${info.message as string}`;
});
const fileFormat = format.combine(