feat: Quests1 (#852)

This commit is contained in:
OrdisPrime 2025-01-24 14:13:21 +01:00 committed by GitHub
parent 8858b15693
commit ef2708b510
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 1804 additions and 1008 deletions

1649
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,56 @@
import { RequestHandler } from "express";
import { isEmptyObject, parseString } from "@/src/helpers/general";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { addKeyChainItems, getInventory } from "@/src/services/inventoryService";
export const giveKeyChainTriggeredItemsController: RequestHandler = async (req, res) => {
const accountId = parseString(req.query.accountId);
const keyChainTriggeredItemsRequest = getJSONfromString(
(req.body as string).toString()
) as IGiveKeyChainTriggeredItemsRequest;
const inventory = await getInventory(accountId);
const inventoryChanges = await addKeyChainItems(inventory, keyChainTriggeredItemsRequest);
if (isEmptyObject(inventoryChanges)) {
throw new Error("inventory changes was empty after getting keychain items: should not happen");
}
// items were added: update quest stage's i (item was given)
const quest = inventory.QuestKeys.find(quest => quest.ItemType === keyChainTriggeredItemsRequest.KeyChain);
if (!quest) {
throw new Error(`Quest ${keyChainTriggeredItemsRequest.KeyChain} not found in QuestKeys`);
}
if (!quest.Progress) {
throw new Error(`Progress should always exist when giving keychain triggered items`);
}
const questStage = quest.Progress[keyChainTriggeredItemsRequest.ChainStage];
if (questStage) {
questStage.i = true;
} else {
const questStageIndex = quest.Progress.push({ i: true }) - 1;
if (questStageIndex !== keyChainTriggeredItemsRequest.ChainStage) {
throw new Error(
`Quest stage index mismatch: ${questStageIndex} !== ${keyChainTriggeredItemsRequest.ChainStage}`
);
}
}
await inventory.save();
res.send(inventoryChanges);
//TODO: Check whether Wishlist is used to track items which should exist uniquely in the inventory
/*
some items are added or removed (not sure) to the wishlist, in that case a
WishlistChanges: ["/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem"],
is added to the response, need to determine for which items this is the case and what purpose this has.
*/
//{"KeyChain":"/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain","ChainStage":0}
//{"WishlistChanges":["/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem"],"MiscItems":[{"ItemType":"/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem","ItemCount":1}]}
};
export interface IGiveKeyChainTriggeredItemsRequest {
KeyChain: string;
ChainStage: number;
}

View File

@ -22,8 +22,6 @@ export const inventorySlotsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req);
//const body = JSON.parse(req.body as string) as IInventorySlotsRequest;
//console.log(body);
//TODO: check which slot was purchased because pvpBonus is also possible
const inventory = await getInventory(accountId);
@ -31,7 +29,5 @@ export const inventorySlotsController: RequestHandler = async (req, res) => {
updateSlots(inventory, InventorySlot.PVE_LOADOUTS, 1, 1);
await inventory.save();
//console.log({ InventoryChanges: currencyChanges }, " added loadout changes:");
res.json({ InventoryChanges: currencyChanges });
};

View File

@ -1,10 +1,13 @@
import { RequestHandler } from "express";
import { missionInventoryUpdate } from "@/src/services/inventoryService";
import { combineRewardAndLootInventory, getRewards } from "@/src/services/missionInventoryUpdateService";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountIdForRequest } from "@/src/services/loginService";
import { IMissionInventoryUpdateRequest } from "@/src/types/requestTypes";
import { logger } from "@/src/utils/logger";
import {
addMissionInventoryUpdates,
addMissionRewards,
calculateFinalCredits
} from "@/src/services/missionInventoryUpdateService";
import { getInventory } from "@/src/services/inventoryService";
/*
**** INPUT ****
- [ ] crossPlaySetting
@ -30,13 +33,13 @@ import { logger } from "@/src/utils/logger";
- [ ] hosts
- [x] ChallengeProgress
- [ ] SeasonChallengeHistory
- [ ] PS (Passive anti-cheat data which includes your username, module list, process list, and system name.)
- [ ] PS (anticheat data)
- [ ] ActiveDojoColorResearch
- [x] RewardInfo
- [ ] ReceivedCeremonyMsg
- [ ] LastCeremonyResetDate
- [ ] MissionPTS (Used to validate the mission/alive time above.)
- [ ] RepHash (A hash from the replication manager/RepMgr Unknown what it does.)
- [ ] RepHash
- [ ] EndOfMatchUpload
- [ ] ObjectiveReached
- [ ] FpsAvg
@ -45,34 +48,52 @@ import { logger } from "@/src/utils/logger";
- [ ] FpsSamples
*/
const missionInventoryUpdateController: RequestHandler = async (req, res): Promise<void> => {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const missionInventoryUpdateController: RequestHandler = async (req, res): Promise<void> => {
const accountId = await getAccountIdForRequest(req);
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call
const lootInventory = getJSONfromString(req.body.toString()) as IMissionInventoryUpdateRequest;
const missionReport = getJSONfromString((req.body as string).toString()) as IMissionInventoryUpdateRequest;
logger.debug("missionInventoryUpdate with lootInventory =", lootInventory);
const { InventoryChanges, MissionRewards } = getRewards(lootInventory);
const { combinedInventoryChanges, TotalCredits, CreditsBonus, MissionCredits, FusionPoints } =
combineRewardAndLootInventory(InventoryChanges, lootInventory);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const InventoryJson = JSON.stringify(await missionInventoryUpdate(combinedInventoryChanges, accountId));
res.json({
// InventoryJson, // this part will reset game data and missions will be locked
MissionRewards,
InventoryChanges,
TotalCredits,
CreditsBonus,
MissionCredits,
FusionPoints
});
} catch (err) {
console.error("Error parsing JSON data:", err);
if (missionReport.MissionStatus !== "GS_SUCCESS") {
console.log(`Mission failed: ${missionReport.RewardInfo?.node}`);
//todo: return expected response for failed mission
res.json([]);
//duvirisadjob does not provide missionStatus
}
const inventory = await getInventory(accountId);
const missionRewardsResults = await addMissionRewards(inventory, missionReport);
if (!missionRewardsResults) {
console.error("Failed to add mission rewards");
res.status(500).json({ error: "Failed to add mission rewards" });
return;
}
const { MissionRewards, inventoryChanges, missionCompletionCredits } = missionRewardsResults;
const inventoryUpdates = addMissionInventoryUpdates(inventory, missionReport);
//todo ? can go after not awaiting
//creditBonus is not correct for mirage mission 3
const credits = calculateFinalCredits(inventory, {
missionCompletionCredits,
missionDropCredits: missionReport.RegularCredits ?? 0,
rngRewardCredits: inventoryChanges.RegularCredits as number
});
const InventoryJson = JSON.stringify((await inventory.save()).toJSON());
//TODO: figure out when to send inventory. it is needed for many cases.
res.json({
InventoryJson,
InventoryChanges: inventoryChanges,
MissionRewards,
...credits,
...inventoryUpdates,
FusionPoints: inventoryChanges.FusionPoints
});
};
/*
@ -85,5 +106,3 @@ const missionInventoryUpdateController: RequestHandler = async (req, res): Promi
- [x] InventoryChanges
- [x] FusionPoints
*/
export { missionInventoryUpdateController };

View File

@ -0,0 +1,12 @@
import { RequestHandler } from "express";
import { updateShipFeature } from "@/src/services/personalRoomsService";
import { IUnlockShipFeatureRequest } from "@/src/types/requestTypes";
import { parseString } from "@/src/helpers/general";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const unlockShipFeatureController: RequestHandler = async (req, res) => {
const accountId = parseString(req.query.accountId);
const shipFeatureRequest = JSON.parse((req.body as string).toString()) as IUnlockShipFeatureRequest;
await updateShipFeature(accountId, shipFeatureRequest.Feature);
res.send([]);
};

View File

@ -0,0 +1,41 @@
import { RequestHandler } from "express";
import { parseString } from "@/src/helpers/general";
import { logger } from "@/src/utils/logger";
import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { updateQuestKey, IUpdateQuestRequest } from "@/src/services/questService";
import { getQuestCompletionItems } from "@/src/services/itemDataService";
import { addItem, combineInventoryChanges, getInventory } from "@/src/services/inventoryService";
// eslint-disable-next-line @typescript-eslint/no-misused-promises
export const updateQuestController: RequestHandler = async (req, res) => {
const accountId = parseString(req.query.accountId);
const updateQuestRequest = getJSONfromString((req.body as string).toString()) as IUpdateQuestRequest;
// updates should be made only to one quest key per request
if (updateQuestRequest.QuestKeys.length > 1) {
throw new Error(`quest keys array should only have 1 item, but has ${updateQuestRequest.QuestKeys.length}`);
}
const inventory = await getInventory(accountId);
updateQuestKey(inventory, updateQuestRequest.QuestKeys);
if (updateQuestRequest.QuestKeys[0].Completed) {
logger.debug(`completed quest ${updateQuestRequest.QuestKeys[0].ItemType} `);
const questKeyName = updateQuestRequest.QuestKeys[0].ItemType;
const questCompletionItems = getQuestCompletionItems(questKeyName);
logger.debug(`quest completion items { ${questCompletionItems.map(item => item.ItemType).join(", ")} }`);
const inventoryChanges = {};
for (const item of questCompletionItems) {
const inventoryDelta = await addItem(inventory, item.ItemType, item.ItemCount);
combineInventoryChanges(inventoryChanges, inventoryDelta.InventoryChanges);
}
res.json({ MissionRewards: [], inventoryChanges });
return;
}
await inventory.save();
res.send({ MissionRewards: [] });
};

View File

@ -26,9 +26,9 @@ import {
IInfestedFoundryDatabase,
IHelminthResource,
IConsumedSuit,
IQuestProgress,
IQuestStage,
IQuestKeyDatabase,
IQuestKeyResponse,
IQuestKeyClient,
IFusionTreasure,
ISpectreLoadout,
IWeaponSkinDatabase,
@ -518,7 +518,7 @@ infestedFoundrySchema.set("toJSON", {
}
});
const questProgressSchema = new Schema<IQuestProgress>({
const questProgressSchema = new Schema<IQuestStage>({
c: Number,
i: Boolean,
m: Boolean,
@ -527,7 +527,7 @@ const questProgressSchema = new Schema<IQuestProgress>({
const questKeysSchema = new Schema<IQuestKeyDatabase>(
{
Progress: [questProgressSchema],
Progress: { type: [questProgressSchema], default: undefined },
unlock: Boolean,
Completed: Boolean,
//CustomData: Schema.Types.Mixed,
@ -544,7 +544,7 @@ questKeysSchema.set("toJSON", {
const questKeysDatabase = ret as IQuestKeyDatabase;
if (questKeysDatabase.CompletionDate) {
(questKeysDatabase as IQuestKeyResponse).CompletionDate = toMongoDate(questKeysDatabase.CompletionDate);
(questKeysDatabase as IQuestKeyClient).CompletionDate = toMongoDate(questKeysDatabase.CompletionDate);
}
}
});
@ -941,6 +941,7 @@ const inventorySchema = new Schema<IInventoryDatabase, InventoryDocumentProps>(
//Complete Mission\Quests
Missions: [Schema.Types.Mixed],
QuestKeys: [questKeysSchema],
ActiveQuest: { type: String, default: "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain" }, //TODO: check after mission starting gear
//item like DojoKey or Boss missions key
LevelKeys: [Schema.Types.Mixed],
//Active quests
@ -1164,7 +1165,7 @@ inventorySchema.set("toJSON", {
});
// type overwrites for subdocuments/subdocument arrays
type InventoryDocumentProps = {
export type InventoryDocumentProps = {
Suits: Types.DocumentArray<IEquipmentDatabase>;
LongGuns: Types.DocumentArray<IEquipmentDatabase>;
Pistols: Types.DocumentArray<IEquipmentDatabase>;

View File

@ -70,7 +70,7 @@ const apartmentSchema = new Schema<IApartment>(
{
Rooms: [roomSchema],
FavouriteLoadouts: [Schema.Types.Mixed],
Gardening: gardeningSchema
Gardening: gardeningSchema // TODO: ensure this is correct
},
{ _id: false }
);
@ -96,7 +96,7 @@ const orbiterSchema = new Schema<IOrbiter>(
{ _id: false }
);
const orbiterDefault: IOrbiter = {
Features: [],
Features: ["/Lotus/Types/Items/ShipFeatureItems/EarthNavigationFeatureItem"], //TODO: potentially remove after missionstarting gear
Rooms: [
{ Name: "AlchemyRoom", MaxCapacity: 1600 },
{ Name: "BridgeRoom", MaxCapacity: 1600 },

View File

@ -78,6 +78,9 @@ import { updateChallengeProgressController } from "@/src/controllers/api/updateC
import { updateSessionGetController, updateSessionPostController } from "@/src/controllers/api/updateSessionController";
import { updateThemeController } from "../controllers/api/updateThemeController";
import { upgradesController } from "@/src/controllers/api/upgradesController";
import { updateQuestController } from "@/src/controllers/api/updateQuestController";
import { giveKeyChainTriggeredItemsController } from "@/src/controllers/api/giveKeyChainTriggeredItemsController";
import { unlockShipFeatureController } from "@/src/controllers/api/unlockShipFeatureController";
const apiRouter = express.Router();
@ -132,6 +135,7 @@ apiRouter.post("/genericUpdate.php", genericUpdateController);
apiRouter.post("/getAlliance.php", getAllianceController);
apiRouter.post("/getVoidProjectionRewards.php", getVoidProjectionRewardsController);
apiRouter.post("/gildWeapon.php", gildWeaponController);
apiRouter.post("/giveKeyChainTriggeredItems.php", giveKeyChainTriggeredItemsController);
apiRouter.post("/guildTech.php", guildTechController);
apiRouter.post("/hostSession.php", hostSessionController);
apiRouter.post("/infestedFoundry.php", infestedFoundryController);
@ -161,10 +165,12 @@ apiRouter.post("/syndicateSacrifice.php", syndicateSacrificeController);
apiRouter.post("/syndicateStandingBonus.php", syndicateStandingBonusController);
apiRouter.post("/tauntHistory.php", tauntHistoryController);
apiRouter.post("/trainingResult.php", trainingResultController);
apiRouter.post("/unlockShipFeature.php", unlockShipFeatureController);
apiRouter.post("/updateChallengeProgress.php", updateChallengeProgressController);
apiRouter.post("/updateNodeIntros.php", genericUpdateController);
apiRouter.post("/updateSession.php", updateSessionPostController);
apiRouter.post("/updateTheme.php", updateThemeController);
apiRouter.post("/updateQuest.php", updateQuestController);
apiRouter.post("/upgrades.php", upgradesController);
export { apiRouter };

View File

@ -1,6 +1,10 @@
import { Inventory, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
import {
Inventory,
InventoryDocumentProps,
TInventoryDatabaseDocument
} from "@/src/models/inventoryModels/inventoryModel";
import { config } from "@/src/services/configService";
import { Types } from "mongoose";
import { HydratedDocument, Types } from "mongoose";
import { SlotNames, IInventoryChanges, IBinChanges, ICurrencyChanges } from "@/src/types/purchaseTypes";
import {
IChallengeProgress,
@ -14,9 +18,9 @@ import {
InventorySlot,
IWeaponSkinClient,
TEquipmentKey,
equipmentKeys,
IFusionTreasure,
IDailyAffiliations
IDailyAffiliations,
IInventoryDatabase
} from "@/src/types/inventoryTypes/inventoryTypes";
import { IGenericUpdate } from "../types/genericUpdate";
import {
@ -25,7 +29,7 @@ import {
IUpdateChallengeProgressRequest
} from "../types/requestTypes";
import { logger } from "@/src/utils/logger";
import { getWeaponType, getExalted } from "@/src/services/itemDataService";
import { getWeaponType, getExalted, getKeyChainItems } from "@/src/services/itemDataService";
import { IEquipmentClient, IItemConfig } from "../types/inventoryTypes/commonInventoryTypes";
import {
ExportArcanes,
@ -39,6 +43,8 @@ import {
TStandingLimitBin
} from "warframe-public-export-plus";
import { createShip } from "./shipService";
import { creditBundles, fusionBundles } from "@/src/services/missionInventoryUpdateService";
import { IGiveKeyChainTriggeredItemsRequest } from "@/src/controllers/api/giveKeyChainTriggeredItemsController";
export const createInventory = async (
accountOwnerId: Types.ObjectId,
@ -64,31 +70,32 @@ export const createInventory = async (
{ ItemCount: 1, ItemType: "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem2" },
{ ItemCount: 1, ItemType: "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem3" },
{ ItemCount: 1, ItemType: "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem4" },
{ ItemCount: 1, ItemType: "/Lotus/Types/Restoratives/LisetAutoHack" },
// Vor's Prize rewards
{ ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Warframe/AvatarHealthMaxMod" },
{ ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Warframe/AvatarShieldMaxMod" },
{ ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Warframe/AvatarAbilityRangeMod" },
{ ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Warframe/AvatarAbilityStrengthMod" },
{ ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Warframe/AvatarAbilityDurationMod" },
{ ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Warframe/AvatarPickupBonusMod" },
{ ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Warframe/AvatarPowerMaxMod" },
{ ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Warframe/AvatarEnemyRadarMod" },
{ ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Melee/WeaponFireRateMod" },
{ ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Melee/WeaponMeleeDamageMod" },
{ ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Rifle/WeaponFactionDamageCorpus" },
{ ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Rifle/WeaponFactionDamageGrineer" },
{ ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Rifle/WeaponDamageAmountMod" },
{ ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Pistol/WeaponFireDamageMod" },
{ ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Pistol/WeaponElectricityDamageMod" },
{ ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Pistol/WeaponDamageAmountMod" },
{ ItemCount: 1, ItemType: "/Lotus/Types/Recipes/Weapons/BurstonRifleBlueprint" },
{ ItemCount: 1, ItemType: "/Lotus/Types/Items/MiscItems/Morphic" },
{ ItemCount: 400, ItemType: "/Lotus/Types/Items/MiscItems/PolymerBundle" },
{ ItemCount: 150, ItemType: "/Lotus/Types/Items/MiscItems/AlloyPlate" }
{ ItemCount: 1, ItemType: "/Lotus/Types/Restoratives/LisetAutoHack" }
];
// const vorsPrizeRewards = [
// // Vor's Prize rewards
// { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Warframe/AvatarHealthMaxMod" },
// { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Warframe/AvatarShieldMaxMod" },
// { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Warframe/AvatarAbilityRangeMod" },
// { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Warframe/AvatarAbilityStrengthMod" },
// { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Warframe/AvatarAbilityDurationMod" },
// { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Warframe/AvatarPickupBonusMod" },
// { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Warframe/AvatarPowerMaxMod" },
// { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Warframe/AvatarEnemyRadarMod" },
// { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Melee/WeaponFireRateMod" },
// { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Melee/WeaponMeleeDamageMod" },
// { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Rifle/WeaponFactionDamageCorpus" },
// { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Rifle/WeaponFactionDamageGrineer" },
// { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Rifle/WeaponDamageAmountMod" },
// { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Pistol/WeaponFireDamageMod" },
// { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Pistol/WeaponElectricityDamageMod" },
// { ItemCount: 1, ItemType: "/Lotus/Upgrades/Mods/Pistol/WeaponDamageAmountMod" },
// { ItemCount: 1, ItemType: "/Lotus/Types/Recipes/Weapons/BurstonRifleBlueprint" },
// { ItemCount: 1, ItemType: "/Lotus/Types/Items/MiscItems/Morphic" },
// { ItemCount: 400, ItemType: "/Lotus/Types/Items/MiscItems/PolymerBundle" },
// { ItemCount: 150, ItemType: "/Lotus/Types/Items/MiscItems/AlloyPlate" }
// ];
for (const equipment of defaultEquipment) {
await addItem(inventory, equipment.ItemType, equipment.ItemCount);
}
@ -109,7 +116,6 @@ export const createInventory = async (
});
inventory.QuestKeys.push({
Completed: true,
ItemType: "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain"
});
@ -132,13 +138,19 @@ export const createInventory = async (
}
};
/**
* Combines two inventory changes objects into one.
*
* @param InventoryChanges - will hold the combined changes
* @param delta - inventory changes to be added
*/
export const combineInventoryChanges = (InventoryChanges: IInventoryChanges, delta: IInventoryChanges): void => {
for (const key in delta) {
if (!(key in InventoryChanges)) {
InventoryChanges[key] = delta[key];
} else if (Array.isArray(delta[key])) {
const left = InventoryChanges[key] as object[];
const right: object[] = delta[key];
const right: object[] | string[] = delta[key];
for (const item of right) {
left.push(item);
}
@ -154,8 +166,10 @@ export const combineInventoryChanges = (InventoryChanges: IInventoryChanges, del
left.Extra ??= 0;
left.Extra += right.Extra;
}
} else if (typeof delta[key] === "number") {
(InventoryChanges[key] as number) += delta[key];
} else {
logger.warn(`inventory change not merged: ${key}`);
throw new Error(`inventory change not merged: unhandled type for inventory key ${key}`);
}
}
};
@ -274,6 +288,24 @@ export const addItem = async (
}
};
}
if (typeName in creditBundles) {
const creditsTotal = creditBundles[typeName] * quantity;
inventory.RegularCredits += creditsTotal;
return {
InventoryChanges: {
RegularCredits: creditsTotal
}
};
}
if (typeName in fusionBundles) {
const fusionPointsTotal = fusionBundles[typeName] * quantity;
inventory.FusionPoints += fusionPointsTotal;
return {
InventoryChanges: {
FusionPoints: fusionPointsTotal
}
};
}
// Path-based duck typing
switch (typeName.substr(1).split("/")[1]) {
@ -364,7 +396,7 @@ export const addItem = async (
}
}
}
case "Game":
case "Game": {
if (typeName.substr(1).split("/")[3] == "Projections") {
// Void Relics, e.g. /Lotus/Types/Game/Projections/T2VoidProjectionGaussPrimeDBronze
const miscItemChanges = [
@ -382,6 +414,40 @@ export const addItem = async (
}
break;
}
case "Keys": {
inventory.QuestKeys.push({ ItemType: typeName });
return {
InventoryChanges: {
QuestKeys: [
{
ItemType: typeName
}
]
}
};
}
case "NeutralCreatures": {
const horseIndex = inventory.Horses.push({ ItemType: typeName });
return {
InventoryChanges: {
Horses: inventory.Horses[horseIndex - 1].toJSON()
}
};
}
case "Recipes": {
inventory.MiscItems.push({ ItemType: typeName, ItemCount: quantity });
return {
InventoryChanges: {
MiscItems: [
{
ItemType: typeName,
ItemCount: quantity
}
]
}
};
}
}
break;
}
const errorMessage = `unable to add item: ${typeName}`;
@ -684,7 +750,8 @@ const addCrewShip = (
return inventoryChanges;
};
const addGearExpByCategory = (
//TODO: wrong id is not erroring
export const addGearExpByCategory = (
inventory: TInventoryDatabaseDocument,
gearArray: IEquipmentClient[] | undefined,
categoryName: TEquipmentKey
@ -870,7 +937,7 @@ export const addChallenges = (
});
};
const addMissionComplete = (inventory: TInventoryDatabaseDocument, { Tag, Completes }: IMission): void => {
export const addMissionComplete = (inventory: TInventoryDatabaseDocument, { Tag, Completes }: IMission): void => {
const { Missions } = inventory;
const itemIndex = Missions.findIndex(item => item.Tag === Tag);
@ -882,83 +949,6 @@ const addMissionComplete = (inventory: TInventoryDatabaseDocument, { Tag, Comple
}
};
export const missionInventoryUpdate = async (data: IMissionInventoryUpdateRequest, accountId: string) => {
const {
RawUpgrades,
MiscItems,
RegularCredits,
ChallengeProgress,
FusionPoints,
Consumables,
Recipes,
Missions,
FusionTreasures
} = data;
const inventory = await getInventory(accountId);
// credits
inventory.RegularCredits += RegularCredits || 0;
// endo
inventory.FusionPoints += FusionPoints || 0;
// syndicate
data.AffiliationChanges?.forEach(affiliation => {
const syndicate = inventory.Affiliations.find(x => x.Tag == affiliation.Tag);
if (syndicate !== undefined) {
syndicate.Standing =
syndicate.Standing === undefined ? affiliation.Standing : syndicate.Standing + affiliation.Standing;
syndicate.Title = syndicate.Title === undefined ? affiliation.Title : syndicate.Title + affiliation.Title;
} else {
inventory.Affiliations.push({
Standing: affiliation.Standing,
Title: affiliation.Title,
Tag: affiliation.Tag,
FreeFavorsEarned: [],
FreeFavorsUsed: []
});
}
});
// Gear XP
equipmentKeys.forEach(key => addGearExpByCategory(inventory, data[key], key));
// Incarnon Challenges
if (data.EvolutionProgress) {
for (const evoProgress of data.EvolutionProgress) {
const entry = inventory.EvolutionProgress
? inventory.EvolutionProgress.find(entry => entry.ItemType == evoProgress.ItemType)
: undefined;
if (entry) {
entry.Progress = evoProgress.Progress;
entry.Rank = evoProgress.Rank;
} else {
inventory.EvolutionProgress ??= [];
inventory.EvolutionProgress.push(evoProgress);
}
}
}
// LastRegionPlayed
if (data.LastRegionPlayed) {
inventory.LastRegionPlayed = data.LastRegionPlayed;
}
// other
addMods(inventory, RawUpgrades);
addMiscItems(inventory, MiscItems);
addConsumables(inventory, Consumables);
addRecipes(inventory, Recipes);
addChallenges(inventory, ChallengeProgress);
addFusionTreasures(inventory, FusionTreasures);
if (Missions) {
addMissionComplete(inventory, Missions);
}
const changedInventory = await inventory.save();
return changedInventory.toJSON();
};
export const addBooster = async (ItemType: string, time: number, accountId: string): Promise<void> => {
const currentTime = Math.floor(Date.now() / 1000) - 129600; // Value is wrong without 129600. Figure out why, please. :)
@ -977,3 +967,52 @@ export const addBooster = async (ItemType: string, time: number, accountId: stri
await inventory.save();
};
export const updateSyndicate = (
inventory: HydratedDocument<IInventoryDatabase, InventoryDocumentProps>,
syndicateUpdate: IMissionInventoryUpdateRequest["AffiliationChanges"]
) => {
syndicateUpdate?.forEach(affiliation => {
const syndicate = inventory.Affiliations.find(x => x.Tag == affiliation.Tag);
if (syndicate !== undefined) {
syndicate.Standing =
syndicate.Standing === undefined ? affiliation.Standing : syndicate.Standing + affiliation.Standing;
syndicate.Title = syndicate.Title === undefined ? affiliation.Title : syndicate.Title + affiliation.Title;
} else {
inventory.Affiliations.push({
Standing: affiliation.Standing,
Title: affiliation.Title,
Tag: affiliation.Tag,
FreeFavorsEarned: [],
FreeFavorsUsed: []
});
}
});
return { AffiliationMods: [] };
};
/**
* @returns object with inventory keys of changes or empty object when no items were added
*/
export const addKeyChainItems = async (
inventory: TInventoryDatabaseDocument,
keyChainData: IGiveKeyChainTriggeredItemsRequest
): Promise<IInventoryChanges> => {
const keyChainItems = getKeyChainItems(keyChainData);
logger.debug(
`adding key chain items ${keyChainItems.join()} for ${keyChainData.KeyChain} at stage ${keyChainData.ChainStage}`
);
const nonStoreItems = keyChainItems.map(item => item.replace("StoreItems/", ""));
//TODO: inventoryChanges is not typed correctly
const inventoryChanges = {};
for (const item of nonStoreItems) {
const inventoryChangesDelta = await addItem(inventory, item);
combineInventoryChanges(inventoryChanges, inventoryChangesDelta.InventoryChanges);
}
return inventoryChanges;
};

View File

@ -1,4 +1,7 @@
import { IGiveKeyChainTriggeredItemsRequest } from "@/src/controllers/api/giveKeyChainTriggeredItemsController";
import { getIndexAfter } from "@/src/helpers/stringHelpers";
import { ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger";
import {
dict_de,
dict_en,
@ -20,13 +23,16 @@ import {
ExportGear,
ExportKeys,
ExportRecipes,
ExportRegions,
ExportResources,
ExportSentinels,
ExportWarframes,
ExportWeapons,
IPowersuit,
IRecipe
IRecipe,
IRegion
} from "warframe-public-export-plus";
import questCompletionItems from "@/static/fixed_responses/questCompletionRewards.json";
export type WeaponTypeInternal =
| "LongGuns"
@ -150,3 +156,56 @@ export const getDict = (lang: string): Record<string, string> => {
export const getString = (key: string, dict: Record<string, string>): string => {
return dict[key] ?? key;
};
export const getKeyChainItems = ({ KeyChain, ChainStage }: IGiveKeyChainTriggeredItemsRequest): string[] => {
const chainStages = ExportKeys[KeyChain].chainStages;
if (!chainStages) {
throw new Error(`KeyChain ${KeyChain} does not contain chain stages`);
}
const keyChainStage = chainStages[ChainStage];
if (!keyChainStage) {
throw new Error(`KeyChainStage ${ChainStage} not found`);
}
if (keyChainStage.itemsToGiveWhenTriggered.length === 0) {
throw new Error(`No items to give for KeyChain ${KeyChain} at stage ${ChainStage}`);
}
return keyChainStage.itemsToGiveWhenTriggered;
};
export const getLevelKeyRewards = (levelKey: string) => {
const levelKeyData = ExportKeys[levelKey];
if (!levelKeyData) {
const error = `LevelKey ${levelKey} not found`;
logger.error(error);
throw new Error(error);
}
if (!levelKeyData.rewards) {
const error = `LevelKey ${levelKey} does not contain rewards`;
logger.error(error);
throw new Error(error);
}
return levelKeyData.rewards;
};
export const getNode = (nodeName: string): IRegion => {
const node = ExportRegions[nodeName];
if (!node) {
throw new Error(`Node ${nodeName} not found`);
}
return node;
};
export const getQuestCompletionItems = (questKey: string) => {
const items = (questCompletionItems as unknown as Record<string, ITypeCount[] | undefined>)[questKey];
if (!items) {
throw new Error(`Quest ${questKey} not found in questCompletionItems`);
}
return items;
};

View File

@ -45,16 +45,16 @@ export const createPersonalRooms = async (accountId: Types.ObjectId, shipId: Typ
activeShipId: shipId
});
if (config.skipTutorial) {
// Vor's Prize rewards
const defaultFeatures = [
"/Lotus/Types/Items/ShipFeatureItems/EarthNavigationFeatureItem",
"/Lotus/Types/Items/ShipFeatureItems/MercuryNavigationFeatureItem",
"/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem",
"/Lotus/Types/Items/ShipFeatureItems/SocialMenuFeatureItem",
"/Lotus/Types/Items/ShipFeatureItems/FoundryFeatureItem",
"/Lotus/Types/Items/ShipFeatureItems/ModsFeatureItem"
];
personalRooms.Ship.Features.push(...defaultFeatures);
// // Vor's Prize rewards
// const defaultFeatures = [
// "/Lotus/Types/Items/ShipFeatureItems/EarthNavigationFeatureItem",
// "/Lotus/Types/Items/ShipFeatureItems/MercuryNavigationFeatureItem",
// "/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem",
// "/Lotus/Types/Items/ShipFeatureItems/SocialMenuFeatureItem",
// "/Lotus/Types/Items/ShipFeatureItems/FoundryFeatureItem",
// "/Lotus/Types/Items/ShipFeatureItems/ModsFeatureItem"
// ];
// personalRooms.Ship.Features.push(...defaultFeatures);
}
await personalRooms.save();
};

View File

@ -1,30 +1,280 @@
import { IMissionRewardResponse, IInventoryFieldType, inventoryFields } from "@/src/types/missionTypes";
import {
ExportRegions,
ExportRewards,
ExportUpgrades,
ExportGear,
ExportRecipes,
ExportRelics,
ExportResources,
IReward
} from "warframe-public-export-plus";
import { IMissionInventoryUpdateRequest } from "../types/requestTypes";
import { ExportRegions, ExportRewards, IReward } from "warframe-public-export-plus";
import { IMissionInventoryUpdateRequest, IRewardInfo } from "../types/requestTypes";
import { logger } from "@/src/utils/logger";
import { IRngResult, getRandomReward } from "@/src/services/rngService";
import { IInventoryDatabase } from "@/src/types/inventoryTypes/inventoryTypes";
import {
addChallenges,
addConsumables,
addFusionTreasures,
addGearExpByCategory,
addItem,
addMiscItems,
addMissionComplete,
addMods,
addRecipes,
combineInventoryChanges,
updateSyndicate
} from "@/src/services/inventoryService";
import { updateQuestKey } from "@/src/services/questService";
import { HydratedDocument } from "mongoose";
import { IInventoryChanges } from "@/src/types/purchaseTypes";
import { getLevelKeyRewards, getNode } from "@/src/services/itemDataService";
import { InventoryDocumentProps, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel";
// need reverse engineer rewardSeed, otherwise ingame displayed rotation reward will be different than added to db or displayed on mission end
const getRewards = ({
RewardInfo
}: IMissionInventoryUpdateRequest): {
InventoryChanges: IMissionInventoryUpdateRequest;
MissionRewards: IMissionRewardResponse[];
} => {
if (!RewardInfo) {
return { InventoryChanges: {}, MissionRewards: [] };
const getRotations = (rotationCount: number): number[] => {
if (rotationCount === 0) return [0];
const rotationPattern = [0, 0, 1, 2]; // A, A, B, C
const rotatedValues = [];
for (let i = 0; i < rotationCount; i++) {
rotatedValues.push(rotationPattern[i % rotationPattern.length]);
}
return rotatedValues;
};
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
};
export const fusionBundles: Record<string, number> = {
"/Lotus/Upgrades/Mods/FusionBundles/CommonFusionBundle": 15,
"/Lotus/Upgrades/Mods/FusionBundles/UncommonFusionBundle": 50,
"/Lotus/Upgrades/Mods/FusionBundles/RareFusionBundle": 80
};
type Entries<T, K extends keyof T = keyof T> = (K extends unknown ? [K, T[K]] : never)[];
function getEntriesUnsafe<T extends object>(object: T): Entries<T> {
return Object.entries(object) as Entries<T>;
}
//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];
//const knownUnhandledKeys: readonly string[] = ["test"] as const; // for unimplemented but important keys
export const addMissionInventoryUpdates = (
inventory: HydratedDocument<IInventoryDatabase, InventoryDocumentProps>,
inventoryUpdates: IMissionInventoryUpdateRequest
) => {
//TODO: type this properly
const inventoryChanges: Partial<IInventoryDatabase> = {};
if (inventoryUpdates.MissionFailed === true) {
return;
}
for (const [key, value] of getEntriesUnsafe(inventoryUpdates)) {
if (value === undefined) {
logger.error(`Inventory update key ${key} has no value `);
continue;
}
switch (key) {
case "RegularCredits":
inventory.RegularCredits += value;
break;
case "QuestKeys":
updateQuestKey(inventory, value);
break;
case "AffiliationChanges":
updateSyndicate(inventory, value);
break;
// Incarnon Challenges
case "EvolutionProgress": {
for (const evoProgress of value) {
const entry = inventory.EvolutionProgress
? inventory.EvolutionProgress.find(entry => entry.ItemType == evoProgress.ItemType)
: undefined;
if (entry) {
entry.Progress = evoProgress.Progress;
entry.Rank = evoProgress.Rank;
} else {
inventory.EvolutionProgress ??= [];
inventory.EvolutionProgress.push(evoProgress);
}
}
break;
}
case "Missions":
addMissionComplete(inventory, value);
break;
case "LastRegionPlayed":
inventory.LastRegionPlayed = value;
break;
case "RawUpgrades":
addMods(inventory, value);
break;
case "MiscItems":
addMiscItems(inventory, value);
break;
case "Consumables":
addConsumables(inventory, value);
break;
case "Recipes":
addRecipes(inventory, value);
break;
case "ChallengeProgress":
addChallenges(inventory, value);
break;
case "FusionTreasures":
addFusionTreasures(inventory, value);
break;
case "FusionBundles": {
let fusionPoints = 0;
for (const fusionBundle of value) {
const fusionPointsTotal = fusionBundles[fusionBundle.ItemType] * fusionBundle.ItemCount;
inventory.FusionPoints += fusionPointsTotal;
fusionPoints += fusionPointsTotal;
}
inventoryChanges.FusionPoints = fusionPoints;
break;
}
// Equipment XP updates
case "Suits":
case "LongGuns":
case "Pistols":
case "Melee":
case "SpecialItems":
case "Sentinels":
case "SentinelWeapons":
case "SpaceSuits":
case "SpaceGuns":
case "SpaceMelee":
case "Hoverboards":
case "OperatorAmps":
case "MoaPets":
addGearExpByCategory(inventory, value, key);
break;
default:
// if (
// (ignoredInventoryUpdateKeys as readonly string[]).includes(key) ||
// knownUnhandledKeys.includes(key)
// ) {
// continue;
// }
// logger.error(`Unhandled inventory update key: ${key}`);
}
}
return inventoryChanges;
};
//TODO: return type of partial missioninventoryupdate response
export const addMissionRewards = async (
inventory: TInventoryDatabaseDocument,
{ RewardInfo: rewardInfo, LevelKeyName: levelKeyName, Missions: missions }: IMissionInventoryUpdateRequest
) => {
if (!rewardInfo) {
logger.error("no reward info provided");
return;
}
//TODO: check double reward merging
const MissionRewards = getRandomMissionDrops(rewardInfo).map(drop => {
return { StoreItem: drop.type, ItemCount: drop.itemCount };
});
console.log("random mission drops:", MissionRewards);
const inventoryChanges: IInventoryChanges = {};
let missionCompletionCredits = 0;
//inventory change is what the client has not rewarded itself, credit updates seem to be taken from totalCredits
if (levelKeyName) {
const fixedLevelRewards = getLevelKeyRewards(levelKeyName);
//logger.debug(`fixedLevelRewards ${fixedLevelRewards}`);
for (const reward of fixedLevelRewards) {
if (reward.rewardType == "RT_CREDITS") {
inventory.RegularCredits += reward.amount;
missionCompletionCredits += reward.amount;
continue;
}
MissionRewards.push({
StoreItem: reward.itemType,
ItemCount: reward.rewardType === "RT_RESOURCE" ? reward.amount : 1
});
}
}
if (missions) {
const levelCreditReward = getLevelCreditRewards(missions?.Tag);
missionCompletionCredits += levelCreditReward;
inventory.RegularCredits += levelCreditReward;
logger.debug(`levelCreditReward ${levelCreditReward}`);
}
//TODO: resolve issue with creditbundles
for (const reward of MissionRewards) {
//TODO: additem should take in storeItems
const inventoryChange = await addItem(inventory, reward.StoreItem.replace("StoreItems/", ""), reward.ItemCount);
//TODO: combineInventoryChanges improve type safety, merging 2 of the same item?
//TODO: check for the case when two of the same item are added, combineInventoryChanges should merge them
//TODO: some conditional types to rule out binchanges?
combineInventoryChanges(inventoryChanges, inventoryChange.InventoryChanges);
}
return { inventoryChanges, MissionRewards, missionCompletionCredits };
};
//might not be faithful to original
//TODO: consider ActiveBoosters
export const calculateFinalCredits = (
inventory: HydratedDocument<IInventoryDatabase>,
{
missionDropCredits,
missionCompletionCredits,
rngRewardCredits = 0
}: { missionDropCredits: number; missionCompletionCredits: number; rngRewardCredits: number }
) => {
const hasDailyCreditBonus = true;
const totalCredits = missionDropCredits + missionCompletionCredits + rngRewardCredits;
const finalCredits = {
MissionCredits: [missionDropCredits, missionDropCredits],
CreditBonus: [missionCompletionCredits, missionCompletionCredits],
TotalCredits: [totalCredits, totalCredits]
};
if (hasDailyCreditBonus) {
inventory.RegularCredits += totalCredits;
finalCredits.CreditBonus[1] *= 2;
finalCredits.MissionCredits[1] *= 2;
finalCredits.TotalCredits[1] *= 2;
}
if (!hasDailyCreditBonus) {
return finalCredits;
}
return { ...finalCredits, DailyMissionBonus: true };
};
function getLevelCreditRewards(nodeName: string): number {
const minEnemyLevel = getNode(nodeName).minEnemyLevel;
return 1000 + (minEnemyLevel - 1) * 100;
//TODO: get dark sektor fixed credit rewards and railjack bonus
}
function getRandomMissionDrops(RewardInfo: IRewardInfo): IRngResult[] {
const drops: IRngResult[] = [];
if (RewardInfo.node in ExportRegions) {
const region = ExportRegions[RewardInfo.node];
@ -53,161 +303,16 @@ const getRewards = ({
});
if (region.cacheRewardManifest && RewardInfo.EnemyCachesFound) {
console.log("cache rewards", RewardInfo.EnemyCachesFound);
const deck = ExportRewards[region.cacheRewardManifest];
for (let rotation = 0; rotation != RewardInfo.EnemyCachesFound; ++rotation) {
const drop = getRandomRewardByChance(deck[rotation]);
if (drop) {
console.log("cache drop", drop);
drops.push(drop);
}
}
}
}
logger.debug("Mission rewards:", drops);
return formatRewardsToInventoryType(drops);
};
const combineRewardAndLootInventory = (
rewardInventory: IMissionInventoryUpdateRequest,
lootInventory: IMissionInventoryUpdateRequest
) => {
const missionCredits = lootInventory.RegularCredits || 0;
const creditsBonus = rewardInventory.RegularCredits || 0;
const totalCredits = missionCredits + creditsBonus;
let FusionPoints = rewardInventory.FusionPoints || 0;
// Discharge Endo picked up during the mission
if (lootInventory.FusionBundles) {
for (const fusionBundle of lootInventory.FusionBundles) {
if (fusionBundle.ItemType in fusionBundles) {
FusionPoints += fusionBundles[fusionBundle.ItemType] * fusionBundle.ItemCount;
} else {
logger.error(`unknown fusion bundle: ${fusionBundle.ItemType}`);
}
}
lootInventory.FusionBundles = undefined;
}
lootInventory.RegularCredits = totalCredits;
lootInventory.FusionPoints = FusionPoints;
inventoryFields.forEach((field: IInventoryFieldType) => {
if (rewardInventory[field] && !lootInventory[field]) {
lootInventory[field] = [];
}
rewardInventory[field]?.forEach(item => lootInventory[field]!.push(item));
});
return {
combinedInventoryChanges: lootInventory,
TotalCredits: [totalCredits, totalCredits],
CreditsBonus: [creditsBonus, creditsBonus],
MissionCredits: [missionCredits, missionCredits],
FusionPoints: FusionPoints
};
};
const getRotations = (rotationCount: number): number[] => {
if (rotationCount === 0) return [0];
const rotationPattern = [0, 0, 1, 2]; // A, A, B, C
const rotatedValues = [];
for (let i = 0; i < rotationCount; i++) {
rotatedValues.push(rotationPattern[i % rotationPattern.length]);
}
return rotatedValues;
};
const getRandomRewardByChance = (pool: IReward[]): IRngResult | undefined => {
return getRandomReward(pool as IRngResult[]);
};
const creditBundles: Record<string, number> = {
"/Lotus/StoreItems/Types/PickUps/Credits/1500Credits": 1500,
"/Lotus/StoreItems/Types/PickUps/Credits/2000Credits": 2000,
"/Lotus/StoreItems/Types/PickUps/Credits/2500Credits": 2500,
"/Lotus/StoreItems/Types/PickUps/Credits/3000Credits": 3000,
"/Lotus/StoreItems/Types/PickUps/Credits/4000Credits": 4000,
"/Lotus/StoreItems/Types/PickUps/Credits/5000Credits": 5000,
"/Lotus/StoreItems/Types/PickUps/Credits/7500Credits": 7500,
"/Lotus/StoreItems/Types/PickUps/Credits/10000Credits": 10000,
"/Lotus/StoreItems/Types/StoreItems/CreditBundles/Zariman/TableACreditsCommon": 15000,
"/Lotus/StoreItems/Types/StoreItems/CreditBundles/Zariman/TableACreditsUncommon": 30000,
"/Lotus/StoreItems/Types/PickUps/Credits/CorpusArenaCreditRewards/CorpusArenaRewardOneHard": 105000,
"/Lotus/StoreItems/Types/PickUps/Credits/CorpusArenaCreditRewards/CorpusArenaRewardTwoHard": 175000,
"/Lotus/StoreItems/Types/PickUps/Credits/CorpusArenaCreditRewards/CorpusArenaRewardThreeHard": 250000
};
const fusionBundles: Record<string, number> = {
"/Lotus/Upgrades/Mods/FusionBundles/CommonFusionBundle": 15,
"/Lotus/Upgrades/Mods/FusionBundles/UncommonFusionBundle": 50,
"/Lotus/Upgrades/Mods/FusionBundles/RareFusionBundle": 80
};
const formatRewardsToInventoryType = (
rewards: IRngResult[]
): { InventoryChanges: IMissionInventoryUpdateRequest; MissionRewards: IMissionRewardResponse[] } => {
const InventoryChanges: IMissionInventoryUpdateRequest = {};
const MissionRewards: IMissionRewardResponse[] = [];
for (const reward of rewards) {
if (reward.type in creditBundles) {
InventoryChanges.RegularCredits ??= 0;
InventoryChanges.RegularCredits += creditBundles[reward.type] * reward.itemCount;
} else {
const type = reward.type.replace("/Lotus/StoreItems/", "/Lotus/");
if (type in fusionBundles) {
InventoryChanges.FusionPoints ??= 0;
InventoryChanges.FusionPoints += fusionBundles[type] * reward.itemCount;
} else if (type in ExportUpgrades) {
addRewardResponse(InventoryChanges, MissionRewards, type, reward.itemCount, "RawUpgrades");
} else if (type in ExportGear) {
addRewardResponse(InventoryChanges, MissionRewards, type, reward.itemCount, "Consumables");
} else if (type in ExportRecipes) {
addRewardResponse(InventoryChanges, MissionRewards, type, reward.itemCount, "Recipes");
} else if (
type in ExportRelics ||
(type in ExportResources && ExportResources[type].productCategory == "MiscItems")
) {
addRewardResponse(InventoryChanges, MissionRewards, type, reward.itemCount, "MiscItems");
} else {
logger.error(`rolled reward ${reward.itemCount}X ${reward.type} but unsure how to give it`);
}
}
}
return { InventoryChanges, MissionRewards };
};
const addRewardResponse = (
InventoryChanges: IMissionInventoryUpdateRequest,
MissionRewards: IMissionRewardResponse[],
ItemType: string,
ItemCount: number,
InventoryCategory: IInventoryFieldType
) => {
if (!ItemType) return;
if (!InventoryChanges[InventoryCategory]) {
InventoryChanges[InventoryCategory] = [];
}
const existReward = InventoryChanges[InventoryCategory].find(item => item.ItemType === ItemType);
if (existReward) {
existReward.ItemCount += ItemCount;
const missionReward = MissionRewards.find(missionReward => missionReward.TypeName === ItemType);
if (missionReward) {
missionReward.ItemCount += ItemCount;
}
} else {
InventoryChanges[InventoryCategory].push({ ItemType, ItemCount });
MissionRewards.push({
ItemCount,
TweetText: ItemType, // ensure if/how this even still used, or if it's needed at all
ProductCategory: InventoryCategory,
StoreItem: ItemType.replace("/Lotus/", "/Lotus/StoreItems/"),
TypeName: ItemType
});
}
};
export { getRewards, combineRewardAndLootInventory };
return drops;
}

View File

@ -1,4 +1,5 @@
import { PersonalRooms } from "@/src/models/personalRoomsModel";
import { addItem, getInventory } from "@/src/services/inventoryService";
export const getPersonalRooms = async (accountId: string) => {
const personalRooms = await PersonalRooms.findOne({ personalRoomsOwnerId: accountId });
@ -8,3 +9,18 @@ export const getPersonalRooms = async (accountId: string) => {
}
return personalRooms;
};
export const updateShipFeature = async (accountId: string, shipFeature: string) => {
const personalRooms = await getPersonalRooms(accountId);
if (personalRooms.Ship.Features.includes(shipFeature)) {
throw new Error(`ship feature ${shipFeature} already unlocked`);
}
personalRooms.Ship.Features.push(shipFeature);
await personalRooms.save();
const inventory = await getInventory(accountId);
await addItem(inventory, shipFeature, -1);
await inventory.save();
};

View File

@ -0,0 +1,34 @@
import { IInventoryDatabase, IQuestKeyDatabase } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger";
import { HydratedDocument } from "mongoose";
export const updateQuestKey = (
inventory: HydratedDocument<IInventoryDatabase>,
questKeyUpdate: IUpdateQuestRequest["QuestKeys"]
): void => {
if (questKeyUpdate.length > 1) {
logger.error(`more than 1 quest key not supported`);
throw new Error("more than 1 quest key not supported");
}
const questKeyIndex = inventory.QuestKeys.findIndex(questKey => questKey.ItemType === questKeyUpdate[0].ItemType);
if (questKeyIndex === -1) {
throw new Error(`quest key ${questKeyUpdate[0].ItemType} not found`);
}
inventory.QuestKeys[questKeyIndex] = questKeyUpdate[0];
if (questKeyUpdate[0].Completed) {
inventory.QuestKeys[questKeyIndex].CompletionDate = new Date();
}
};
export interface IUpdateQuestRequest {
QuestKeys: Omit<IQuestKeyDatabase, "CompletionDate">[];
PS: string;
questCompletion: boolean;
PlayerShipEvents: unknown[];
crossPlaySetting: string;
DoQuestReward: boolean;
}

View File

@ -43,6 +43,7 @@ export interface IInventoryDatabase
GuildId?: Types.ObjectId; // GuildId changed from ?IOid to ?Types.ObjectId
PendingRecipes: IPendingRecipe[];
QuestKeys: IQuestKeyDatabase[];
ActiveQuest: string;
BlessingCooldown: Date;
Ships: Types.ObjectId[];
WeaponSkins: IWeaponSkinDatabase[];
@ -71,7 +72,7 @@ export interface IInventoryDatabase
}
export interface IQuestKeyDatabase {
Progress?: IQuestProgress[];
Progress?: IQuestStage[];
unlock?: boolean;
Completed?: boolean;
CustomData?: string; //TODO: check whether this actually exists
@ -205,7 +206,7 @@ export interface IInventoryClient extends IDailyAffiliations {
RawUpgrades: IRawUpgrade[];
ReceivedStartingGear: boolean;
Ships: IShipInventory[];
QuestKeys: IQuestKeyResponse[];
QuestKeys: IQuestKeyClient[];
FlavourItems: IFlavourItem[];
Scoops: IEquipmentDatabase[];
TrainingRetriesLeft: number;
@ -889,14 +890,14 @@ export interface IPlayerSkills {
LPS_DRIFT_ENDURANCE: number;
}
export interface IQuestKeyResponse extends Omit<IQuestKeyDatabase, "CompletionDate"> {
export interface IQuestKeyClient extends Omit<IQuestKeyDatabase, "CompletionDate"> {
CompletionDate?: IMongoDate;
}
export interface IQuestProgress {
c: number;
i: boolean;
m: boolean;
export interface IQuestStage {
c?: number;
i?: boolean;
m?: boolean;
b?: any[];
}

View File

@ -1,11 +1,11 @@
export const inventoryFields = ["RawUpgrades", "MiscItems", "Consumables", "Recipes"] as const;
export type IInventoryFieldType = (typeof inventoryFields)[number];
export interface IMissionRewardResponse {
StoreItem?: string;
TypeName: string;
export interface IMissionReward {
StoreItem: string;
TypeName?: string;
UpgradeLevel?: number;
ItemCount: number;
TweetText: string;
ProductCategory: string;
TweetText?: string;
ProductCategory?: string;
}

View File

@ -3,16 +3,15 @@ import { ArtifactPolarity, IPolarity, IEquipmentClient } from "@/src/types/inven
import {
IBooster,
IChallengeProgress,
IConsumable,
IEvolutionProgress,
IMiscItem,
ITypeCount,
IMission,
IRawUpgrade,
ISeasonChallenge,
TSolarMapRegion,
TEquipmentKey,
IFusionTreasure
IFusionTreasure,
IQuestKeyClient
} from "./inventoryTypes/inventoryTypes";
export interface IThemeUpdateRequest {
@ -33,10 +32,13 @@ export interface IUpdateChallengeProgressRequest {
SeasonChallengeCompletions: ISeasonChallenge[];
}
export interface IMissionInventoryUpdateRequest {
rewardsMultiplier?: number;
ActiveBoosters?: IBooster[];
export type IMissionInventoryUpdateRequest = {
AffiliationChanges?: IAffiliationChange[];
crossPlaySetting?: string;
rewardsMultiplier?: number;
GoalTag: string;
LevelKeyName: string;
ActiveBoosters?: IBooster[];
Suits?: IEquipmentClient[];
LongGuns?: IEquipmentClient[];
Pistols?: IEquipmentClient[];
@ -52,21 +54,40 @@ export interface IMissionInventoryUpdateRequest {
MoaPets?: IEquipmentClient[];
FusionBundles?: ITypeCount[];
RawUpgrades?: IRawUpgrade[];
MiscItems?: IMiscItem[];
Consumables?: IConsumable[];
MiscItems?: ITypeCount[];
Consumables?: ITypeCount[];
FusionTreasures?: IFusionTreasure[];
Recipes?: IConsumable[];
Recipes?: ITypeCount[];
QuestKeys?: IQuestKeyClient[];
RegularCredits?: number;
ChallengeProgress?: IChallengeProgress[];
RewardInfo?: IMissionInventoryUpdateRequestRewardInfo;
MissionFailed: boolean;
MissionStatus: IMissionStatus;
AliveTime: number;
MissionTime: number;
Missions?: IMission;
EvolutionProgress?: IEvolutionProgress[];
LastRegionPlayed?: TSolarMapRegion;
GameModeId: number;
hosts: string[];
currentClients: unknown[];
ChallengeProgress: IChallengeProgress[];
PS: string;
ActiveDojoColorResearch: string;
RewardInfo?: IRewardInfo;
ReceivedCeremonyMsg: boolean;
LastCeremonyResetDate: number;
MissionPTS: number;
RepHash: string;
EndOfMatchUpload: boolean;
ObjectiveReached: boolean;
sharedSessionId: string;
FpsAvg: number;
FpsMin: number;
FpsMax: number;
FpsSamples: number;
EvolutionProgress?: IEvolutionProgress[];
};
FusionPoints?: number; // Not a part of the request, but we put it in this struct as an intermediate storage.
}
export interface IMissionInventoryUpdateRequestRewardInfo {
export interface IRewardInfo {
node: string;
VaultsCracked?: number; // for Spy missions
rewardTier?: number;
@ -82,15 +103,15 @@ export interface IMissionInventoryUpdateRequestRewardInfo {
rewardSeed?: number;
}
export type IMissionStatus = "GS_SUCCESS" | "GS_FAILURE" | "GS_DUMPED" | "GS_QUIT" | "GS_INTERRUPTED";
export interface IInventorySlotsRequest {
Bin: "PveBonusLoadoutBin";
}
export interface IUpdateGlyphRequest {
AvatarImageType: string;
AvatarImage: string;
}
export interface IUpgradesRequest {
ItemCategory: TEquipmentKey;
ItemId: IOid;
@ -98,7 +119,6 @@ export interface IUpgradesRequest {
UpgradeVersion: number;
Operations: IUpgradeOperation[];
}
export interface IUpgradeOperation {
OperationType: string;
UpgradeRequirement: string; // uniqueName of item being consumed
@ -106,3 +126,8 @@ export interface IUpgradeOperation {
PolarizeValue: ArtifactPolarity;
PolarityRemap: IPolarity[];
}
export interface IUnlockShipFeatureRequest {
Feature: string;
KeyChain: string;
ChainStage: number;
}

View File

@ -0,0 +1,13 @@
{
"/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain": [
{
"ItemType": "/Lotus/Types/Keys/DuviriQuest/DuviriQuestKeyChain",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/NeutralCreatures/ErsatzHorse/ErsatzHorsePowerSuit",
"ItemCount": 1
}
],
"/Lotus/Types/Keys/InfestedMicroplanetQuest/InfestedMicroplanetQuestKeyChain": [{ "ItemType": "/Lotus/Types/Recipes/WarframeRecipes/BrokenFrameBlueprint", "ItemCount": 1 }]
}