feat: Quests1 #852

Merged
OrdisPrime merged 8 commits from missioninventoryupdate-refactor-1 into main 2025-01-24 05:13:21 -08:00
19 changed files with 1804 additions and 1008 deletions

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

View File

@ -1,10 +1,13 @@
import { RequestHandler } from "express"; import { RequestHandler } from "express";
import { missionInventoryUpdate } from "@/src/services/inventoryService";
import { combineRewardAndLootInventory, getRewards } from "@/src/services/missionInventoryUpdateService";
import { getJSONfromString } from "@/src/helpers/stringHelpers"; import { getJSONfromString } from "@/src/helpers/stringHelpers";
import { getAccountIdForRequest } from "@/src/services/loginService"; import { getAccountIdForRequest } from "@/src/services/loginService";
import { IMissionInventoryUpdateRequest } from "@/src/types/requestTypes"; 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 **** **** INPUT ****
- [ ] crossPlaySetting - [ ] crossPlaySetting
@ -30,13 +33,13 @@ import { logger } from "@/src/utils/logger";
- [ ] hosts - [ ] hosts
- [x] ChallengeProgress - [x] ChallengeProgress
- [ ] SeasonChallengeHistory - [ ] SeasonChallengeHistory
- [ ] PS (Passive anti-cheat data which includes your username, module list, process list, and system name.) - [ ] PS (anticheat data)
- [ ] ActiveDojoColorResearch - [ ] ActiveDojoColorResearch
- [x] RewardInfo - [x] RewardInfo
- [ ] ReceivedCeremonyMsg - [ ] ReceivedCeremonyMsg
- [ ] LastCeremonyResetDate - [ ] LastCeremonyResetDate
- [ ] MissionPTS (Used to validate the mission/alive time above.) - [ ] MissionPTS (Used to validate the mission/alive time above.)
- [ ] RepHash (A hash from the replication manager/RepMgr Unknown what it does.) - [ ] RepHash
- [ ] EndOfMatchUpload - [ ] EndOfMatchUpload
- [ ] ObjectiveReached - [ ] ObjectiveReached
- [ ] FpsAvg - [ ] FpsAvg
@ -45,34 +48,52 @@ import { logger } from "@/src/utils/logger";
- [ ] FpsSamples - [ ] 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); const accountId = await getAccountIdForRequest(req);
try { const missionReport = getJSONfromString((req.body as string).toString()) as IMissionInventoryUpdateRequest;
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call
const lootInventory = getJSONfromString(req.body.toString()) as IMissionInventoryUpdateRequest;
logger.debug("missionInventoryUpdate with lootInventory =", lootInventory); if (missionReport.MissionStatus !== "GS_SUCCESS") {
console.log(`Mission failed: ${missionReport.RewardInfo?.node}`);
const { InventoryChanges, MissionRewards } = getRewards(lootInventory); //todo: return expected response for failed mission
res.json([]);
const { combinedInventoryChanges, TotalCredits, CreditsBonus, MissionCredits, FusionPoints } = //duvirisadjob does not provide missionStatus
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);
} }
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] InventoryChanges
- [x] FusionPoints - [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([]);
};
coderabbitai[bot] commented 2025-01-24 05:03:47 -08:00 (Migrated from github.com)
Review

🛠️ Refactor suggestion

Add error handling for updateShipFeature.

This controller depends on updateShipFeature succeeding. If an error is thrown (e.g., feature is already unlocked), the request will hang without returning a meaningful error to the client. Consider wrapping this call in a try/catch or using an error-handling middleware to ensure consistent client responses.

_:hammer_and_wrench: Refactor suggestion_ **Add error handling for `updateShipFeature`.** This controller depends on `updateShipFeature` succeeding. If an error is thrown (e.g., feature is already unlocked), the request will hang without returning a meaningful error to the client. Consider wrapping this call in a try/catch or using an error-handling middleware to ensure consistent client responses. <!-- This is an auto-generated comment by CodeRabbit -->

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: [] });
};
coderabbitai[bot] commented 2025-01-24 05:03:48 -08:00 (Migrated from github.com)
Review

🛠️ Refactor suggestion

Consider transactional or atomic updates.

Inventory modifications and quest completions could benefit from a transactional approach (e.g., two-phase commit) to avoid partial updates during concurrent requests or unexpected errors in the middle of the update loop. This helps maintain consistency between quest data and inventory.

_:hammer_and_wrench: Refactor suggestion_ **Consider transactional or atomic updates.** Inventory modifications and quest completions could benefit from a transactional approach (e.g., two-phase commit) to avoid partial updates during concurrent requests or unexpected errors in the middle of the update loop. This helps maintain consistency between quest data and inventory. <!-- This is an auto-generated comment by CodeRabbit -->

View File

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

View File

@ -70,7 +70,7 @@ const apartmentSchema = new Schema<IApartment>(
{ {
Rooms: [roomSchema], Rooms: [roomSchema],
FavouriteLoadouts: [Schema.Types.Mixed], FavouriteLoadouts: [Schema.Types.Mixed],
Gardening: gardeningSchema Gardening: gardeningSchema // TODO: ensure this is correct
}, },
{ _id: false } { _id: false }
); );
@ -96,7 +96,7 @@ const orbiterSchema = new Schema<IOrbiter>(
{ _id: false } { _id: false }
); );
const orbiterDefault: IOrbiter = { const orbiterDefault: IOrbiter = {
Features: [], Features: ["/Lotus/Types/Items/ShipFeatureItems/EarthNavigationFeatureItem"], //TODO: potentially remove after missionstarting gear
Rooms: [ Rooms: [
{ Name: "AlchemyRoom", MaxCapacity: 1600 }, { Name: "AlchemyRoom", MaxCapacity: 1600 },
{ Name: "BridgeRoom", 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 { updateSessionGetController, updateSessionPostController } from "@/src/controllers/api/updateSessionController";
import { updateThemeController } from "../controllers/api/updateThemeController"; import { updateThemeController } from "../controllers/api/updateThemeController";
import { upgradesController } from "@/src/controllers/api/upgradesController"; 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(); const apiRouter = express.Router();
@ -132,6 +135,7 @@ apiRouter.post("/genericUpdate.php", genericUpdateController);
apiRouter.post("/getAlliance.php", getAllianceController); apiRouter.post("/getAlliance.php", getAllianceController);
apiRouter.post("/getVoidProjectionRewards.php", getVoidProjectionRewardsController); apiRouter.post("/getVoidProjectionRewards.php", getVoidProjectionRewardsController);
apiRouter.post("/gildWeapon.php", gildWeaponController); apiRouter.post("/gildWeapon.php", gildWeaponController);
apiRouter.post("/giveKeyChainTriggeredItems.php", giveKeyChainTriggeredItemsController);
apiRouter.post("/guildTech.php", guildTechController); apiRouter.post("/guildTech.php", guildTechController);
apiRouter.post("/hostSession.php", hostSessionController); apiRouter.post("/hostSession.php", hostSessionController);
apiRouter.post("/infestedFoundry.php", infestedFoundryController); apiRouter.post("/infestedFoundry.php", infestedFoundryController);
@ -161,10 +165,12 @@ apiRouter.post("/syndicateSacrifice.php", syndicateSacrificeController);
apiRouter.post("/syndicateStandingBonus.php", syndicateStandingBonusController); apiRouter.post("/syndicateStandingBonus.php", syndicateStandingBonusController);
apiRouter.post("/tauntHistory.php", tauntHistoryController); apiRouter.post("/tauntHistory.php", tauntHistoryController);
apiRouter.post("/trainingResult.php", trainingResultController); apiRouter.post("/trainingResult.php", trainingResultController);
apiRouter.post("/unlockShipFeature.php", unlockShipFeatureController);
apiRouter.post("/updateChallengeProgress.php", updateChallengeProgressController); apiRouter.post("/updateChallengeProgress.php", updateChallengeProgressController);
apiRouter.post("/updateNodeIntros.php", genericUpdateController); apiRouter.post("/updateNodeIntros.php", genericUpdateController);
apiRouter.post("/updateSession.php", updateSessionPostController); apiRouter.post("/updateSession.php", updateSessionPostController);
apiRouter.post("/updateTheme.php", updateThemeController); apiRouter.post("/updateTheme.php", updateThemeController);
apiRouter.post("/updateQuest.php", updateQuestController);
apiRouter.post("/upgrades.php", upgradesController); apiRouter.post("/upgrades.php", upgradesController);
export { apiRouter }; 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 { config } from "@/src/services/configService";
import { Types } from "mongoose"; import { HydratedDocument, Types } from "mongoose";
import { SlotNames, IInventoryChanges, IBinChanges, ICurrencyChanges } from "@/src/types/purchaseTypes"; import { SlotNames, IInventoryChanges, IBinChanges, ICurrencyChanges } from "@/src/types/purchaseTypes";
import { import {
IChallengeProgress, IChallengeProgress,
@ -14,9 +18,9 @@ import {
InventorySlot, InventorySlot,
IWeaponSkinClient, IWeaponSkinClient,
TEquipmentKey, TEquipmentKey,
equipmentKeys,
IFusionTreasure, IFusionTreasure,
IDailyAffiliations IDailyAffiliations,
IInventoryDatabase
} from "@/src/types/inventoryTypes/inventoryTypes"; } from "@/src/types/inventoryTypes/inventoryTypes";
import { IGenericUpdate } from "../types/genericUpdate"; import { IGenericUpdate } from "../types/genericUpdate";
import { import {
@ -25,7 +29,7 @@ import {
IUpdateChallengeProgressRequest IUpdateChallengeProgressRequest
} from "../types/requestTypes"; } from "../types/requestTypes";
import { logger } from "@/src/utils/logger"; 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 { IEquipmentClient, IItemConfig } from "../types/inventoryTypes/commonInventoryTypes";
import { import {
ExportArcanes, ExportArcanes,
@ -39,6 +43,8 @@ import {
TStandingLimitBin TStandingLimitBin
} from "warframe-public-export-plus"; } from "warframe-public-export-plus";
import { createShip } from "./shipService"; import { createShip } from "./shipService";
import { creditBundles, fusionBundles } from "@/src/services/missionInventoryUpdateService";
import { IGiveKeyChainTriggeredItemsRequest } from "@/src/controllers/api/giveKeyChainTriggeredItemsController";
export const createInventory = async ( export const createInventory = async (
accountOwnerId: Types.ObjectId, 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/AvatarImageItem2" },
{ ItemCount: 1, ItemType: "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem3" }, { ItemCount: 1, ItemType: "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem3" },
{ ItemCount: 1, ItemType: "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem4" }, { ItemCount: 1, ItemType: "/Lotus/Types/StoreItems/AvatarImages/AvatarImageItem4" },
{ ItemCount: 1, ItemType: "/Lotus/Types/Restoratives/LisetAutoHack" }, { 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" }
]; ];
// 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) { for (const equipment of defaultEquipment) {
await addItem(inventory, equipment.ItemType, equipment.ItemCount); await addItem(inventory, equipment.ItemType, equipment.ItemCount);
} }
@ -109,7 +116,6 @@ export const createInventory = async (
}); });
inventory.QuestKeys.push({ inventory.QuestKeys.push({
Completed: true,
ItemType: "/Lotus/Types/Keys/VorsPrize/VorsPrizeQuestKeyChain" 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 => { export const combineInventoryChanges = (InventoryChanges: IInventoryChanges, delta: IInventoryChanges): void => {
for (const key in delta) { for (const key in delta) {
if (!(key in InventoryChanges)) { if (!(key in InventoryChanges)) {
InventoryChanges[key] = delta[key]; InventoryChanges[key] = delta[key];
} else if (Array.isArray(delta[key])) { } else if (Array.isArray(delta[key])) {
const left = InventoryChanges[key] as object[]; const left = InventoryChanges[key] as object[];
const right: object[] = delta[key]; const right: object[] | string[] = delta[key];
for (const item of right) { for (const item of right) {
left.push(item); left.push(item);
} }
@ -154,8 +166,10 @@ export const combineInventoryChanges = (InventoryChanges: IInventoryChanges, del
left.Extra ??= 0; left.Extra ??= 0;
left.Extra += right.Extra; left.Extra += right.Extra;
} }
} else if (typeof delta[key] === "number") {
(InventoryChanges[key] as number) += delta[key];
} else { } 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 // Path-based duck typing
switch (typeName.substr(1).split("/")[1]) { 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") { if (typeName.substr(1).split("/")[3] == "Projections") {
// Void Relics, e.g. /Lotus/Types/Game/Projections/T2VoidProjectionGaussPrimeDBronze // Void Relics, e.g. /Lotus/Types/Game/Projections/T2VoidProjectionGaussPrimeDBronze
const miscItemChanges = [ const miscItemChanges = [
@ -381,6 +413,40 @@ export const addItem = async (
}; };
} }
break; 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; break;
} }
@ -684,7 +750,8 @@ const addCrewShip = (
return inventoryChanges; return inventoryChanges;
}; };
const addGearExpByCategory = ( //TODO: wrong id is not erroring
export const addGearExpByCategory = (
inventory: TInventoryDatabaseDocument, inventory: TInventoryDatabaseDocument,
gearArray: IEquipmentClient[] | undefined, gearArray: IEquipmentClient[] | undefined,
categoryName: TEquipmentKey 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 { Missions } = inventory;
const itemIndex = Missions.findIndex(item => item.Tag === Tag); 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> => { 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. :) 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(); 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 { getIndexAfter } from "@/src/helpers/stringHelpers";
import { ITypeCount } from "@/src/types/inventoryTypes/inventoryTypes";
import { logger } from "@/src/utils/logger";
import { import {
dict_de, dict_de,
dict_en, dict_en,
@ -20,13 +23,16 @@ import {
ExportGear, ExportGear,
ExportKeys, ExportKeys,
ExportRecipes, ExportRecipes,
ExportRegions,
ExportResources, ExportResources,
ExportSentinels, ExportSentinels,
ExportWarframes, ExportWarframes,
ExportWeapons, ExportWeapons,
IPowersuit, IPowersuit,
IRecipe IRecipe,
IRegion
} from "warframe-public-export-plus"; } from "warframe-public-export-plus";
import questCompletionItems from "@/static/fixed_responses/questCompletionRewards.json";
export type WeaponTypeInternal = export type WeaponTypeInternal =
| "LongGuns" | "LongGuns"
@ -150,3 +156,56 @@ export const getDict = (lang: string): Record<string, string> => {
export const getString = (key: string, dict: Record<string, string>): string => { export const getString = (key: string, dict: Record<string, string>): string => {
return dict[key] ?? key; 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 activeShipId: shipId
}); });
if (config.skipTutorial) { if (config.skipTutorial) {
// Vor's Prize rewards // // Vor's Prize rewards
const defaultFeatures = [ // const defaultFeatures = [
"/Lotus/Types/Items/ShipFeatureItems/EarthNavigationFeatureItem", // "/Lotus/Types/Items/ShipFeatureItems/EarthNavigationFeatureItem",
"/Lotus/Types/Items/ShipFeatureItems/MercuryNavigationFeatureItem", // "/Lotus/Types/Items/ShipFeatureItems/MercuryNavigationFeatureItem",
"/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem", // "/Lotus/Types/Items/ShipFeatureItems/ArsenalFeatureItem",
"/Lotus/Types/Items/ShipFeatureItems/SocialMenuFeatureItem", // "/Lotus/Types/Items/ShipFeatureItems/SocialMenuFeatureItem",
"/Lotus/Types/Items/ShipFeatureItems/FoundryFeatureItem", // "/Lotus/Types/Items/ShipFeatureItems/FoundryFeatureItem",
"/Lotus/Types/Items/ShipFeatureItems/ModsFeatureItem" // "/Lotus/Types/Items/ShipFeatureItems/ModsFeatureItem"
]; // ];
personalRooms.Ship.Features.push(...defaultFeatures); // personalRooms.Ship.Features.push(...defaultFeatures);
} }
await personalRooms.save(); await personalRooms.save();
}; };

View File

@ -1,30 +1,280 @@
import { IMissionRewardResponse, IInventoryFieldType, inventoryFields } from "@/src/types/missionTypes"; import { ExportRegions, ExportRewards, IReward } from "warframe-public-export-plus";
import { IMissionInventoryUpdateRequest, IRewardInfo } from "../types/requestTypes";
import {
ExportRegions,
ExportRewards,
ExportUpgrades,
ExportGear,
ExportRecipes,
ExportRelics,
ExportResources,
IReward
} from "warframe-public-export-plus";
import { IMissionInventoryUpdateRequest } from "../types/requestTypes";
import { logger } from "@/src/utils/logger"; import { logger } from "@/src/utils/logger";
import { IRngResult, getRandomReward } from "@/src/services/rngService"; 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 getRotations = (rotationCount: number): number[] => {
const getRewards = ({ if (rotationCount === 0) return [0];
RewardInfo
}: IMissionInventoryUpdateRequest): { const rotationPattern = [0, 0, 1, 2]; // A, A, B, C
InventoryChanges: IMissionInventoryUpdateRequest; const rotatedValues = [];
MissionRewards: IMissionRewardResponse[];
} => { for (let i = 0; i < rotationCount; i++) {
if (!RewardInfo) { rotatedValues.push(rotationPattern[i % rotationPattern.length]);
return { InventoryChanges: {}, MissionRewards: [] };
} }
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[] = []; const drops: IRngResult[] = [];
if (RewardInfo.node in ExportRegions) { if (RewardInfo.node in ExportRegions) {
const region = ExportRegions[RewardInfo.node]; const region = ExportRegions[RewardInfo.node];
@ -53,161 +303,16 @@ const getRewards = ({
}); });
if (region.cacheRewardManifest && RewardInfo.EnemyCachesFound) { if (region.cacheRewardManifest && RewardInfo.EnemyCachesFound) {
console.log("cache rewards", RewardInfo.EnemyCachesFound);
const deck = ExportRewards[region.cacheRewardManifest]; const deck = ExportRewards[region.cacheRewardManifest];
for (let rotation = 0; rotation != RewardInfo.EnemyCachesFound; ++rotation) { for (let rotation = 0; rotation != RewardInfo.EnemyCachesFound; ++rotation) {
const drop = getRandomRewardByChance(deck[rotation]); const drop = getRandomRewardByChance(deck[rotation]);
if (drop) { if (drop) {
console.log("cache drop", drop);
drops.push(drop); drops.push(drop);
} }
} }
} }
} }
return drops;
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 };

View File

@ -1,4 +1,5 @@
import { PersonalRooms } from "@/src/models/personalRoomsModel"; import { PersonalRooms } from "@/src/models/personalRoomsModel";
import { addItem, getInventory } from "@/src/services/inventoryService";
export const getPersonalRooms = async (accountId: string) => { export const getPersonalRooms = async (accountId: string) => {
const personalRooms = await PersonalRooms.findOne({ personalRoomsOwnerId: accountId }); const personalRooms = await PersonalRooms.findOne({ personalRoomsOwnerId: accountId });
@ -8,3 +9,18 @@ export const getPersonalRooms = async (accountId: string) => {
} }
return personalRooms; 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();
}
};
coderabbitai[bot] commented 2025-01-24 05:03:47 -08:00 (Migrated from github.com)
Review

⚠️ Potential issue

Handle empty questKeyUpdate arrays.

The logic calls questKeyUpdate[0] without checking if questKeyUpdate is empty. An empty array will cause a runtime error. Add a safeguard to handle this scenario before attempting to read questKeyUpdate[0].

 if (questKeyUpdate.length === 0) {
   throw new Error("No questKey provided for update");
 }
 if (questKeyUpdate.length > 1) {
   logger.error(`more than 1 quest key not supported`);
   throw new Error("more than 1 quest key not supported");
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

export const updateQuestKey = (
    inventory: HydratedDocument<IInventoryDatabase>,
    questKeyUpdate: IUpdateQuestRequest["QuestKeys"]
): void => {
    if (questKeyUpdate.length === 0) {
        throw new Error("No questKey provided for update");
    }
    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();
    }
};
_:warning: Potential issue_ **Handle empty `questKeyUpdate` arrays.** The logic calls `questKeyUpdate[0]` without checking if `questKeyUpdate` is empty. An empty array will cause a runtime error. Add a safeguard to handle this scenario before attempting to read `questKeyUpdate[0]`. ```diff if (questKeyUpdate.length === 0) { throw new Error("No questKey provided for update"); } if (questKeyUpdate.length > 1) { logger.error(`more than 1 quest key not supported`); throw new Error("more than 1 quest key not supported"); } ``` <!-- suggestion_start --> <details> <summary>📝 Committable suggestion</summary> > ‼️ **IMPORTANT** > Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements. `````suggestion export const updateQuestKey = ( inventory: HydratedDocument<IInventoryDatabase>, questKeyUpdate: IUpdateQuestRequest["QuestKeys"] ): void => { if (questKeyUpdate.length === 0) { throw new Error("No questKey provided for update"); } 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(); } }; ````` </details> <!-- suggestion_end --> <!-- This is an auto-generated comment by CodeRabbit -->
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 GuildId?: Types.ObjectId; // GuildId changed from ?IOid to ?Types.ObjectId
PendingRecipes: IPendingRecipe[]; PendingRecipes: IPendingRecipe[];
QuestKeys: IQuestKeyDatabase[]; QuestKeys: IQuestKeyDatabase[];
ActiveQuest: string;
BlessingCooldown: Date; BlessingCooldown: Date;
Ships: Types.ObjectId[]; Ships: Types.ObjectId[];
WeaponSkins: IWeaponSkinDatabase[]; WeaponSkins: IWeaponSkinDatabase[];
@ -71,7 +72,7 @@ export interface IInventoryDatabase
} }
export interface IQuestKeyDatabase { export interface IQuestKeyDatabase {
Progress?: IQuestProgress[]; Progress?: IQuestStage[];
unlock?: boolean; unlock?: boolean;
Completed?: boolean; Completed?: boolean;
CustomData?: string; //TODO: check whether this actually exists CustomData?: string; //TODO: check whether this actually exists
@ -205,7 +206,7 @@ export interface IInventoryClient extends IDailyAffiliations {
RawUpgrades: IRawUpgrade[]; RawUpgrades: IRawUpgrade[];
ReceivedStartingGear: boolean; ReceivedStartingGear: boolean;
Ships: IShipInventory[]; Ships: IShipInventory[];
QuestKeys: IQuestKeyResponse[]; QuestKeys: IQuestKeyClient[];
FlavourItems: IFlavourItem[]; FlavourItems: IFlavourItem[];
Scoops: IEquipmentDatabase[]; Scoops: IEquipmentDatabase[];
TrainingRetriesLeft: number; TrainingRetriesLeft: number;
@ -889,14 +890,14 @@ export interface IPlayerSkills {
LPS_DRIFT_ENDURANCE: number; LPS_DRIFT_ENDURANCE: number;
} }
export interface IQuestKeyResponse extends Omit<IQuestKeyDatabase, "CompletionDate"> { export interface IQuestKeyClient extends Omit<IQuestKeyDatabase, "CompletionDate"> {
CompletionDate?: IMongoDate; CompletionDate?: IMongoDate;
} }
export interface IQuestProgress { export interface IQuestStage {
c: number; c?: number;
i: boolean; i?: boolean;
m: boolean; m?: boolean;
b?: any[]; b?: any[];
} }

View File

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

View File

@ -3,16 +3,15 @@ import { ArtifactPolarity, IPolarity, IEquipmentClient } from "@/src/types/inven
import { import {
IBooster, IBooster,
IChallengeProgress, IChallengeProgress,
IConsumable,
IEvolutionProgress, IEvolutionProgress,
IMiscItem,
ITypeCount, ITypeCount,
IMission, IMission,
IRawUpgrade, IRawUpgrade,
ISeasonChallenge, ISeasonChallenge,
TSolarMapRegion, TSolarMapRegion,
TEquipmentKey, TEquipmentKey,
IFusionTreasure IFusionTreasure,
IQuestKeyClient
} from "./inventoryTypes/inventoryTypes"; } from "./inventoryTypes/inventoryTypes";
export interface IThemeUpdateRequest { export interface IThemeUpdateRequest {
@ -33,10 +32,13 @@ export interface IUpdateChallengeProgressRequest {
SeasonChallengeCompletions: ISeasonChallenge[]; SeasonChallengeCompletions: ISeasonChallenge[];
} }
export interface IMissionInventoryUpdateRequest { export type IMissionInventoryUpdateRequest = {
rewardsMultiplier?: number;
ActiveBoosters?: IBooster[];
AffiliationChanges?: IAffiliationChange[]; AffiliationChanges?: IAffiliationChange[];
crossPlaySetting?: string;
rewardsMultiplier?: number;
GoalTag: string;
LevelKeyName: string;
ActiveBoosters?: IBooster[];
Suits?: IEquipmentClient[]; Suits?: IEquipmentClient[];
LongGuns?: IEquipmentClient[]; LongGuns?: IEquipmentClient[];
Pistols?: IEquipmentClient[]; Pistols?: IEquipmentClient[];
@ -52,21 +54,40 @@ export interface IMissionInventoryUpdateRequest {
MoaPets?: IEquipmentClient[]; MoaPets?: IEquipmentClient[];
FusionBundles?: ITypeCount[]; FusionBundles?: ITypeCount[];
RawUpgrades?: IRawUpgrade[]; RawUpgrades?: IRawUpgrade[];
MiscItems?: IMiscItem[]; MiscItems?: ITypeCount[];
Consumables?: IConsumable[]; Consumables?: ITypeCount[];
FusionTreasures?: IFusionTreasure[]; FusionTreasures?: IFusionTreasure[];
Recipes?: IConsumable[]; Recipes?: ITypeCount[];
QuestKeys?: IQuestKeyClient[];
RegularCredits?: number; RegularCredits?: number;
ChallengeProgress?: IChallengeProgress[]; MissionFailed: boolean;
RewardInfo?: IMissionInventoryUpdateRequestRewardInfo; MissionStatus: IMissionStatus;
AliveTime: number;
MissionTime: number;
Missions?: IMission; Missions?: IMission;
EvolutionProgress?: IEvolutionProgress[];
LastRegionPlayed?: TSolarMapRegion; 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 IRewardInfo {
}
export interface IMissionInventoryUpdateRequestRewardInfo {
node: string; node: string;
VaultsCracked?: number; // for Spy missions VaultsCracked?: number; // for Spy missions
rewardTier?: number; rewardTier?: number;
@ -82,15 +103,15 @@ export interface IMissionInventoryUpdateRequestRewardInfo {
rewardSeed?: number; rewardSeed?: number;
} }
export type IMissionStatus = "GS_SUCCESS" | "GS_FAILURE" | "GS_DUMPED" | "GS_QUIT" | "GS_INTERRUPTED";
export interface IInventorySlotsRequest { export interface IInventorySlotsRequest {
Bin: "PveBonusLoadoutBin"; Bin: "PveBonusLoadoutBin";
} }
export interface IUpdateGlyphRequest { export interface IUpdateGlyphRequest {
AvatarImageType: string; AvatarImageType: string;
coderabbitai[bot] commented 2025-01-24 05:06:48 -08:00 (Migrated from github.com)
Review

🛠️ Refactor suggestion

Improve type safety for client-related fields.

  • hosts array should probably have a more specific type than string[]
  • currentClients using unknown[] loses type safety. Consider defining a proper client type.
-    hosts: string[];
-    currentClients: unknown[];
+    hosts: string[]; // TODO: Consider using specific host type/enum
+    currentClients: IGameClient[]; // Define proper client interface

Committable suggestion skipped: line range outside the PR's diff.

_:hammer_and_wrench: Refactor suggestion_ **Improve type safety for client-related fields.** - `hosts` array should probably have a more specific type than `string[]` - `currentClients` using `unknown[]` loses type safety. Consider defining a proper client type. ```diff - hosts: string[]; - currentClients: unknown[]; + hosts: string[]; // TODO: Consider using specific host type/enum + currentClients: IGameClient[]; // Define proper client interface ``` > Committable suggestion skipped: line range outside the PR's diff. <!-- This is an auto-generated comment by CodeRabbit -->
AvatarImage: string; AvatarImage: string;
} }
export interface IUpgradesRequest { export interface IUpgradesRequest {
ItemCategory: TEquipmentKey; ItemCategory: TEquipmentKey;
ItemId: IOid; ItemId: IOid;
@ -98,7 +119,6 @@ export interface IUpgradesRequest {
UpgradeVersion: number; UpgradeVersion: number;
Operations: IUpgradeOperation[]; Operations: IUpgradeOperation[];
} }
export interface IUpgradeOperation { export interface IUpgradeOperation {
OperationType: string; OperationType: string;
UpgradeRequirement: string; // uniqueName of item being consumed UpgradeRequirement: string; // uniqueName of item being consumed
@ -106,3 +126,8 @@ export interface IUpgradeOperation {
PolarizeValue: ArtifactPolarity; PolarizeValue: ArtifactPolarity;
PolarityRemap: IPolarity[]; PolarityRemap: IPolarity[];
coderabbitai[bot] commented 2025-01-24 05:06:47 -08:00 (Migrated from github.com)
Review

🛠️ Refactor suggestion

Make FPS metrics optional.

FPS-related fields should be optional as they might not always be available, especially in different game modes or platforms.

-    FpsAvg: number;
-    FpsMin: number;
-    FpsMax: number;
-    FpsSamples: number;
+    FpsAvg?: number;
+    FpsMin?: number;
+    FpsMax?: number;
+    FpsSamples?: number;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

    FpsAvg?: number;
    FpsMin?: number;
    FpsMax?: number;
    FpsSamples?: number;
_:hammer_and_wrench: Refactor suggestion_ **Make FPS metrics optional.** FPS-related fields should be optional as they might not always be available, especially in different game modes or platforms. ```diff - FpsAvg: number; - FpsMin: number; - FpsMax: number; - FpsSamples: number; + FpsAvg?: number; + FpsMin?: number; + FpsMax?: number; + FpsSamples?: number; ``` <!-- suggestion_start --> <details> <summary>📝 Committable suggestion</summary> > ‼️ **IMPORTANT** > Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements. `````suggestion FpsAvg?: number; FpsMin?: number; FpsMax?: number; FpsSamples?: number; ````` </details> <!-- suggestion_end --> <!-- This is an auto-generated comment by CodeRabbit -->
} }
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 }]
}