2025-02-24 08:50:07 -08:00
import {
2025-03-09 07:42:55 -07:00
ExportEnemies ,
2025-02-24 08:50:07 -08:00
ExportFusionBundles ,
ExportRegions ,
ExportRewards ,
IMissionReward as IMissionRewardExternal ,
2025-03-31 04:15:00 -07:00
IRegion ,
2025-02-24 08:50:07 -08:00
IReward
} from "warframe-public-export-plus" ;
2025-01-24 14:13:21 +01:00
import { IMissionInventoryUpdateRequest , IRewardInfo } from "../types/requestTypes" ;
2024-01-06 16:26:58 +01:00
import { logger } from "@/src/utils/logger" ;
2025-02-28 12:36:01 -08:00
import { IRngResult , getRandomElement , getRandomReward } from "@/src/services/rngService" ;
2025-04-05 06:52:35 -07:00
import { equipmentKeys , TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes" ;
2025-01-24 14:13:21 +01:00
import {
2025-03-24 01:38:32 -07:00
addBooster ,
2025-01-24 14:13:21 +01:00
addChallenges ,
addConsumables ,
2025-02-05 12:23:35 -08:00
addCrewShipAmmo ,
addCrewShipRawSalvage ,
2025-03-07 00:41:18 -08:00
addEmailItem ,
2025-01-27 18:11:05 +01:00
addFocusXpIncreases ,
2025-01-24 14:13:21 +01:00
addFusionTreasures ,
addGearExpByCategory ,
2025-03-09 07:42:55 -07:00
addItem ,
2025-01-24 14:13:21 +01:00
addMiscItems ,
addMissionComplete ,
addMods ,
addRecipes ,
2025-03-22 01:15:09 -07:00
addShipDecorations ,
2025-01-24 14:13:21 +01:00
combineInventoryChanges ,
2025-04-03 06:17:11 -07:00
updateCurrency ,
2025-01-24 14:13:21 +01:00
updateSyndicate
} from "@/src/services/inventoryService" ;
import { updateQuestKey } from "@/src/services/questService" ;
2025-04-05 06:52:35 -07:00
import { Types } from "mongoose" ;
2025-01-24 14:13:21 +01:00
import { IInventoryChanges } from "@/src/types/purchaseTypes" ;
2025-03-31 04:15:00 -07:00
import { getLevelKeyRewards , toStoreItem } from "@/src/services/itemDataService" ;
2025-04-05 06:52:35 -07:00
import { TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel" ;
2025-01-24 16:17:59 +01:00
import { getEntriesUnsafe } from "@/src/utils/ts-utils" ;
2025-01-27 13:18:16 +01:00
import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes" ;
2025-02-23 03:54:46 -08:00
import { handleStoreItemAcquisition } from "./purchaseService" ;
2025-02-24 08:50:07 -08:00
import { IMissionReward } from "../types/missionTypes" ;
2025-02-25 04:38:47 -08:00
import { crackRelic } from "@/src/helpers/relicHelper" ;
2025-02-28 18:09:37 -08:00
import { createMessage } from "./inboxService" ;
import kuriaMessage50 from "@/static/fixed_responses/kuriaMessages/fiftyPercent.json" ;
import kuriaMessage75 from "@/static/fixed_responses/kuriaMessages/seventyFivePercent.json" ;
import kuriaMessage100 from "@/static/fixed_responses/kuriaMessages/oneHundredPercent.json" ;
2025-03-26 16:08:33 -07:00
import conservationAnimals from "@/static/fixed_responses/conservationAnimals.json" ;
2025-03-22 06:08:00 -07:00
import { getInfNodes } from "@/src/helpers/nemesisHelpers" ;
2025-03-31 04:14:20 -07:00
import { Loadout } from "../models/inventoryModels/loadoutModel" ;
2025-04-01 02:29:29 -07:00
import { ILoadoutConfigDatabase } from "../types/saveLoadoutTypes" ;
2025-01-24 14:13:21 +01:00
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 ;
} ;
2023-09-06 14:02:54 +04:00
2025-01-24 14:13:21 +01:00
const getRandomRewardByChance = ( pool : IReward [ ] ) : IRngResult | undefined = > {
return getRandomReward ( pool as IRngResult [ ] ) ;
} ;
//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
2025-03-07 00:41:18 -08:00
export const addMissionInventoryUpdates = async (
2025-04-05 06:52:35 -07:00
inventory : TInventoryDatabaseDocument ,
2025-01-24 14:13:21 +01:00
inventoryUpdates : IMissionInventoryUpdateRequest
2025-03-08 06:29:05 -08:00
) : Promise < IInventoryChanges > = > {
const inventoryChanges : IInventoryChanges = { } ;
2025-04-04 06:02:55 -07:00
if (
inventoryUpdates . EndOfMatchUpload &&
inventoryUpdates . Missions &&
inventoryUpdates . Missions . Tag in ExportRegions
) {
const node = ExportRegions [ inventoryUpdates . Missions . Tag ] ;
if ( node . miscItemFee ) {
addMiscItems ( inventory , [
{
ItemType : node.miscItemFee.ItemType ,
ItemCount : node.miscItemFee.ItemCount * - 1
}
] ) ;
}
}
2025-04-04 02:46:32 +02:00
if ( inventoryUpdates . RewardInfo ) {
if ( inventoryUpdates . RewardInfo . periodicMissionTag ) {
const tag = inventoryUpdates . RewardInfo . periodicMissionTag ;
const existingCompletion = inventory . PeriodicMissionCompletions . find ( completion = > completion . tag === tag ) ;
2025-02-09 09:39:45 -08:00
2025-04-04 02:46:32 +02:00
if ( existingCompletion ) {
existingCompletion . date = new Date ( ) ;
} else {
inventory . PeriodicMissionCompletions . push ( {
tag : tag ,
date : new Date ( )
} ) ;
}
}
if ( inventoryUpdates . RewardInfo . NemesisAbandonedRewards ) {
inventory . NemesisAbandonedRewards = inventoryUpdates . RewardInfo . NemesisAbandonedRewards ;
2025-02-09 09:39:45 -08:00
}
2025-03-08 05:36:06 -08:00
}
2025-03-31 04:14:20 -07:00
if (
inventoryUpdates . MissionFailed &&
inventoryUpdates . MissionStatus == "GS_FAILURE" &&
inventoryUpdates . EndOfMatchUpload &&
2025-04-01 02:29:29 -07:00
inventoryUpdates . ObjectiveReached &&
! inventoryUpdates . LockedWeaponGroup
2025-03-31 04:14:20 -07:00
) {
const loadout = ( await Loadout . findById ( inventory . LoadOutPresets , "NORMAL" ) ) ! ;
const config = loadout . NORMAL . id ( inventory . CurrentLoadOutIds [ 0 ] . $oid ) ! ;
const SuitId = new Types . ObjectId ( config . s ! . ItemId . $oid ) ;
inventory . BrandedSuits ? ? = [ ] ;
if ( ! inventory . BrandedSuits . find ( x = > x . equals ( SuitId ) ) ) {
inventory . BrandedSuits . push ( SuitId ) ;
2025-03-31 09:18:00 -07:00
await createMessage ( inventory . accountOwnerId , [
2025-03-31 04:14:20 -07:00
{
sndr : "/Lotus/Language/Menu/Mailbox_WarframeSender" ,
msg : "/Lotus/Language/G1Quests/BrandedMessage" ,
sub : "/Lotus/Language/G1Quests/BrandedTitle" ,
att : [ "/Lotus/Types/Recipes/Components/BrandRemovalBlueprint" ] ,
2025-03-31 09:18:00 -07:00
highPriority : true // TOVERIFY: I cannot find any content of this within the last 10 years so I can only assume that highPriority is set (it certainly would make sense), but I just don't know for sure that it is so on live.
2025-03-31 04:14:20 -07:00
}
] ) ;
}
}
2025-01-24 14:13:21 +01:00
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" :
2025-03-10 16:22:02 -07:00
await updateQuestKey ( inventory , value ) ;
2025-01-24 14:13:21 +01:00
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" :
2025-02-05 12:23:35 -08:00
case "BonusMiscItems" :
2025-01-24 14:13:21 +01:00
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 ;
2025-02-05 12:23:35 -08:00
case "CrewShipRawSalvage" :
addCrewShipRawSalvage ( inventory , value ) ;
break ;
case "CrewShipAmmo" :
addCrewShipAmmo ( inventory , value ) ;
break ;
2025-03-22 01:15:09 -07:00
case "ShipDecorations" :
// e.g. when getting a 50+ score in happy zephyr, this is how the poster is given.
addShipDecorations ( inventory , value ) ;
break ;
2025-01-24 14:13:21 +01:00
case "FusionBundles" : {
let fusionPoints = 0 ;
for ( const fusionBundle of value ) {
2025-02-23 03:54:26 -08:00
const fusionPointsTotal =
ExportFusionBundles [ fusionBundle . ItemType ] . fusionPoints * fusionBundle . ItemCount ;
2025-01-24 14:13:21 +01:00
inventory . FusionPoints += fusionPointsTotal ;
fusionPoints += fusionPointsTotal ;
}
inventoryChanges . FusionPoints = fusionPoints ;
break ;
}
2025-03-07 00:41:18 -08:00
case "EmailItems" : {
for ( const tc of value ) {
await addEmailItem ( inventory , tc . ItemType ) ;
}
break ;
}
2025-01-27 18:11:05 +01:00
case "FocusXpIncreases" : {
addFocusXpIncreases ( inventory , value ) ;
break ;
}
2025-01-31 17:03:14 +01:00
case "PlayerSkillGains" : {
inventory . PlayerSkills . LPP_SPACE += value . LPP_SPACE ;
inventory . PlayerSkills . LPP_DRIFTER += value . LPP_DRIFTER ;
break ;
}
2025-02-01 07:41:34 -08:00
case "CustomMarkers" : {
value . forEach ( markers = > {
const map = inventory . CustomMarkers
? inventory . CustomMarkers . find ( entry = > entry . tag == markers . tag )
: undefined ;
if ( map ) {
map . markerInfos = markers . markerInfos ;
} else {
inventory . CustomMarkers ? ? = [ ] ;
inventory . CustomMarkers . push ( markers ) ;
}
} ) ;
break ;
}
2025-02-20 02:57:23 -08:00
case "LoreFragmentScans" :
2025-02-25 17:31:24 -08:00
value . forEach ( clientFragment = > {
const fragment = inventory . LoreFragmentScans . find ( x = > x . ItemType == clientFragment . ItemType ) ;
if ( fragment ) {
fragment . Progress += clientFragment . Progress ;
} else {
inventory . LoreFragmentScans . push ( clientFragment ) ;
}
2025-02-20 02:57:23 -08:00
} ) ;
break ;
2025-02-25 17:31:52 -08:00
case "LibraryScans" :
value . forEach ( scan = > {
2025-03-07 00:40:22 -08:00
let synthesisIgnored = true ;
if (
inventory . LibraryPersonalTarget &&
libraryPersonalTargetToAvatar [ inventory . LibraryPersonalTarget ] == scan . EnemyType
) {
let progress = inventory . LibraryPersonalProgress . find (
x = > x . TargetType == inventory . LibraryPersonalTarget
) ;
if ( ! progress ) {
progress =
inventory . LibraryPersonalProgress [
inventory . LibraryPersonalProgress . push ( {
TargetType : inventory.LibraryPersonalTarget ,
Scans : 0 ,
Completed : false
} ) - 1
] ;
2025-02-25 17:31:52 -08:00
}
2025-03-07 00:40:22 -08:00
progress . Scans += scan . Count ;
if (
progress . Scans >=
( inventory . LibraryPersonalTarget ==
"/Lotus/Types/Game/Library/Targets/DragonframeQuestTarget"
? 3
: 10 )
) {
progress . Completed = true ;
}
logger . debug ( ` synthesis of ${ scan . EnemyType } added to personal target progress ` ) ;
synthesisIgnored = false ;
}
if (
inventory . LibraryActiveDailyTaskInfo &&
inventory . LibraryActiveDailyTaskInfo . EnemyTypes . find ( x = > x == scan . EnemyType )
) {
inventory . LibraryActiveDailyTaskInfo . Scans ? ? = 0 ;
inventory . LibraryActiveDailyTaskInfo . Scans += scan . Count ;
logger . debug ( ` synthesis of ${ scan . EnemyType } added to daily task progress ` ) ;
synthesisIgnored = false ;
}
if ( synthesisIgnored ) {
logger . warn ( ` ignoring synthesis of ${ scan . EnemyType } due to not knowing why you did that ` ) ;
2025-02-25 17:31:52 -08:00
}
} ) ;
break ;
2025-02-28 18:09:37 -08:00
case "CollectibleScans" :
2025-03-07 00:41:18 -08:00
for ( const scan of value ) {
2025-02-28 18:09:37 -08:00
const entry = inventory . CollectibleSeries ? . find ( x = > x . CollectibleType == scan . CollectibleType ) ;
if ( entry ) {
entry . Count = scan . Count ;
entry . Tracking = scan . Tracking ;
if ( entry . CollectibleType == "/Lotus/Objects/Orokin/Props/CollectibleSeriesOne" ) {
const progress = entry . Count / entry . ReqScans ;
2025-03-07 00:41:18 -08:00
for ( const gate of entry . IncentiveStates ) {
2025-02-28 18:09:37 -08:00
gate . complete = progress >= gate . threshold ;
if ( gate . complete && ! gate . sent ) {
gate . sent = true ;
if ( gate . threshold == 0.5 ) {
2025-03-31 09:18:00 -07:00
await createMessage ( inventory . accountOwnerId , [ kuriaMessage50 ] ) ;
2025-02-28 18:09:37 -08:00
} else {
2025-03-31 09:18:00 -07:00
await createMessage ( inventory . accountOwnerId , [ kuriaMessage75 ] ) ;
2025-02-28 18:09:37 -08:00
}
}
2025-03-07 00:41:18 -08:00
}
2025-02-28 18:09:37 -08:00
if ( progress >= 1.0 ) {
2025-03-31 09:18:00 -07:00
await createMessage ( inventory . accountOwnerId , [ kuriaMessage100 ] ) ;
2025-02-28 18:09:37 -08:00
}
}
} else {
logger . warn ( ` ${ scan . CollectibleType } was not found in inventory, ignoring scans ` ) ;
}
2025-03-07 00:41:18 -08:00
}
2025-02-28 18:09:37 -08:00
break ;
2025-02-27 18:01:06 -08:00
case "Upgrades" :
value . forEach ( clientUpgrade = > {
const upgrade = inventory . Upgrades . id ( clientUpgrade . ItemId . $oid ) ! ;
upgrade . UpgradeFingerprint = clientUpgrade . UpgradeFingerprint ; // primitive way to copy over the riven challenge progress
} ) ;
break ;
2025-03-24 01:38:32 -07:00
case "Boosters" :
value . forEach ( booster = > {
addBooster ( booster . ItemType , booster . ExpiryDate , inventory ) ;
} ) ;
break ;
2025-02-09 09:39:45 -08:00
case "SyndicateId" : {
inventory . CompletedSyndicates . push ( value ) ;
break ;
}
case "SortieId" : {
inventory . CompletedSorties . push ( value ) ;
break ;
}
2025-02-24 20:56:27 -08:00
case "SeasonChallengeCompletions" : {
2025-02-09 09:39:45 -08:00
const processedCompletions = value . map ( ( { challenge , id } ) = > ( {
challenge : challenge.substring ( challenge . lastIndexOf ( "/" ) + 1 ) ,
id
} ) ) ;
inventory . SeasonChallengeHistory . push ( . . . processedCompletions ) ;
break ;
2025-02-24 20:56:27 -08:00
}
2025-03-16 04:33:21 -07:00
case "DeathMarks" : {
for ( const deathMark of value ) {
if ( ! inventory . DeathMarks . find ( x = > x == deathMark ) ) {
// It's a new death mark; we have to say the line.
2025-03-31 09:18:00 -07:00
await createMessage ( inventory . accountOwnerId , [
2025-03-16 04:33:21 -07:00
{
sub : "/Lotus/Language/G1Quests/DeathMarkTitle" ,
sndr : "/Lotus/Language/G1Quests/DeathMarkSender" ,
msg : "/Lotus/Language/G1Quests/DeathMarkMessage" ,
icon : "/Lotus/Interface/Icons/Npcs/Stalker_d.png" ,
highPriority : true
}
] ) ;
// TODO: This type of inbox message seems to automatically delete itself. Figure out under which conditions.
}
}
inventory . DeathMarks = value ;
break ;
}
2025-03-26 16:08:33 -07:00
case "CapturedAnimals" : {
for ( const capturedAnimal of value ) {
const meta = conservationAnimals [ capturedAnimal . AnimalType as keyof typeof conservationAnimals ] ;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if ( meta ) {
if ( capturedAnimal . NumTags ) {
addMiscItems ( inventory , [
{
ItemType : meta.tag ,
ItemCount : capturedAnimal.NumTags
}
] ) ;
}
if ( capturedAnimal . NumExtraRewards ) {
if ( "extraReward" in meta ) {
addMiscItems ( inventory , [
{
ItemType : meta.extraReward ,
ItemCount : capturedAnimal.NumExtraRewards
}
] ) ;
} else {
logger . warn (
` client attempted to claim unknown extra rewards for conservation of ${ capturedAnimal . AnimalType } `
) ;
}
}
} else {
logger . warn ( ` ignoring conservation of unknown AnimalType: ${ capturedAnimal . AnimalType } ` ) ;
}
}
break ;
}
2025-03-27 03:32:50 -07:00
case "DiscoveredMarkers" : {
for ( const clientMarker of value ) {
const dbMarker = inventory . DiscoveredMarkers . find ( x = > x . tag == clientMarker . tag ) ;
if ( dbMarker ) {
dbMarker . discoveryState = clientMarker . discoveryState ;
} else {
inventory . DiscoveredMarkers . push ( clientMarker ) ;
}
}
break ;
}
2025-04-01 02:29:29 -07:00
case "LockedWeaponGroup" : {
inventory . LockedWeaponGroup = {
s : new Types . ObjectId ( value . s . $oid ) ,
l : value.l ? new Types . ObjectId ( value . l . $oid ) : undefined ,
p : value.p ? new Types . ObjectId ( value . p . $oid ) : undefined ,
m : value.m ? new Types . ObjectId ( value . m . $oid ) : undefined ,
sn : value.sn ? new Types . ObjectId ( value . sn . $oid ) : undefined
} ;
break ;
}
case "UnlockWeapons" : {
inventory . LockedWeaponGroup = undefined ;
break ;
}
case "CurrentLoadOutIds" : {
2025-04-01 15:48:40 -07:00
if ( value . LoadOuts ) {
const loadout = await Loadout . findOne ( { loadoutOwnerId : inventory.accountOwnerId } ) ;
if ( loadout ) {
for ( const [ loadoutId , loadoutConfig ] of Object . entries ( value . LoadOuts . NORMAL ) ) {
const { ItemId , . . . loadoutConfigItemIdRemoved } = loadoutConfig ;
const loadoutConfigDatabase : ILoadoutConfigDatabase = {
_id : new Types . ObjectId ( ItemId . $oid ) ,
. . . loadoutConfigItemIdRemoved
} ;
2025-04-06 10:18:01 -07:00
const dbConfig = loadout . NORMAL . id ( loadoutId ) ;
if ( dbConfig ) {
dbConfig . overwrite ( loadoutConfigDatabase ) ;
} else {
logger . warn ( ` couldn't update loadout because there's no config with id ${ loadoutId } ` ) ;
}
2025-04-01 15:48:40 -07:00
}
await loadout . save ( ) ;
2025-04-01 02:29:29 -07:00
}
}
break ;
}
2025-04-03 06:17:11 -07:00
case "creditsFee" : {
updateCurrency ( inventory , value , false ) ;
inventoryChanges . RegularCredits ? ? = 0 ;
inventoryChanges . RegularCredits -= value ;
break ;
}
2025-01-24 14:13:21 +01:00
default :
2025-01-27 13:18:16 +01:00
// Equipment XP updates
if ( equipmentKeys . includes ( key as TEquipmentKey ) ) {
addGearExpByCategory ( inventory , value as IEquipmentClient [ ] , key as TEquipmentKey ) ;
}
break ;
2025-01-24 14:13:21 +01:00
// if (
// (ignoredInventoryUpdateKeys as readonly string[]).includes(key) ||
// knownUnhandledKeys.includes(key)
// ) {
// continue;
// }
// logger.error(`Unhandled inventory update key: ${key}`);
}
}
return inventoryChanges ;
} ;
2025-03-15 03:24:39 -07:00
interface AddMissionRewardsReturnType {
MissionRewards : IMissionReward [ ] ;
inventoryChanges? : IInventoryChanges ;
credits? : IMissionCredits ;
}
2025-01-24 14:13:21 +01:00
//TODO: return type of partial missioninventoryupdate response
export const addMissionRewards = async (
inventory : TInventoryDatabaseDocument ,
2025-02-06 07:11:31 -08:00
{
2025-03-22 06:08:00 -07:00
Nemesis : nemesis ,
2025-02-06 07:11:31 -08:00
RewardInfo : rewardInfo ,
LevelKeyName : levelKeyName ,
Missions : missions ,
2025-02-25 04:38:47 -08:00
RegularCredits : creditDrops ,
2025-03-09 07:42:55 -07:00
VoidTearParticipantsCurrWave : voidTearWave ,
StrippedItems : strippedItems
2025-02-06 07:11:31 -08:00
} : IMissionInventoryUpdateRequest
2025-03-15 03:24:39 -07:00
) : Promise < AddMissionRewardsReturnType > = > {
2025-01-24 14:13:21 +01:00
if ( ! rewardInfo ) {
2025-02-06 07:11:31 -08:00
//TODO: if there is a case where you can have credits collected during a mission but no rewardInfo, add credits needs to be handled earlier
logger . debug ( ` Mission ${ missions ! . Tag } did not have Reward Info ` ) ;
return { MissionRewards : [ ] } ;
2025-01-24 14:13:21 +01:00
}
//TODO: check double reward merging
2025-02-25 04:42:49 -08:00
const MissionRewards : IMissionReward [ ] = getRandomMissionDrops ( rewardInfo ) ;
2025-02-07 04:53:26 -08:00
logger . debug ( "random mission drops:" , MissionRewards ) ;
2025-01-24 14:13:21 +01:00
const inventoryChanges : IInventoryChanges = { } ;
let missionCompletionCredits = 0 ;
2025-02-06 07:11:31 -08:00
//inventory change is what the client has not rewarded itself, also the client needs to know the credit changes for display
2025-01-24 14:13:21 +01:00
if ( levelKeyName ) {
const fixedLevelRewards = getLevelKeyRewards ( levelKeyName ) ;
//logger.debug(`fixedLevelRewards ${fixedLevelRewards}`);
2025-02-24 08:50:07 -08:00
if ( fixedLevelRewards . levelKeyRewards ) {
addFixedLevelRewards ( fixedLevelRewards . levelKeyRewards , inventory , MissionRewards ) ;
}
if ( fixedLevelRewards . levelKeyRewards2 ) {
for ( const reward of fixedLevelRewards . levelKeyRewards2 ) {
//quest stage completion credit rewards
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
} ) ;
2025-01-24 14:13:21 +01:00
}
}
}
2025-03-31 04:15:00 -07:00
// ignoring tags not in ExportRegions, because it can just be garbage:
// - https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1013
// - https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1365
if ( missions && missions . Tag in ExportRegions ) {
const node = ExportRegions [ missions . Tag ] ;
2025-02-24 08:50:07 -08:00
//node based credit rewards for mission completion
if ( node . missionIndex !== 28 ) {
2025-03-31 04:15:00 -07:00
const levelCreditReward = getLevelCreditRewards ( node ) ;
2025-02-24 08:50:07 -08:00
missionCompletionCredits += levelCreditReward ;
inventory . RegularCredits += levelCreditReward ;
logger . debug ( ` levelCreditReward ${ levelCreditReward } ` ) ;
2025-02-21 06:32:05 -08:00
}
2025-02-24 08:50:07 -08:00
if ( node . missionReward ) {
missionCompletionCredits += addFixedLevelRewards ( node . missionReward , inventory , MissionRewards ) ;
2025-02-21 06:32:05 -08:00
}
}
2025-02-28 12:36:01 -08:00
if ( rewardInfo . useVaultManifest ) {
MissionRewards . push ( {
StoreItem : getRandomElement ( corruptedMods ) ,
ItemCount : 1
} ) ;
}
2025-02-06 07:11:31 -08:00
for ( const reward of MissionRewards ) {
2025-04-03 10:37:52 -07:00
const inventoryChange = await handleStoreItemAcquisition (
reward . StoreItem ,
inventory ,
reward . ItemCount ,
undefined ,
true
) ;
2025-02-06 07:11:31 -08:00
//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, but the client also merges them
//TODO: some conditional types to rule out binchanges?
combineInventoryChanges ( inventoryChanges , inventoryChange . InventoryChanges ) ;
}
const credits = addCredits ( inventory , {
missionCompletionCredits ,
missionDropCredits : creditDrops ? ? 0 ,
rngRewardCredits : inventoryChanges.RegularCredits ? ? 0
} ) ;
2025-01-24 14:13:21 +01:00
2025-02-25 04:38:47 -08:00
if (
voidTearWave &&
voidTearWave . Participants [ 0 ] . QualifiesForReward &&
! voidTearWave . Participants [ 0 ] . HaveRewardResponse
) {
2025-03-09 07:43:30 -07:00
const reward = await crackRelic ( inventory , voidTearWave . Participants [ 0 ] , inventoryChanges ) ;
2025-02-26 15:42:13 -08:00
MissionRewards . push ( { StoreItem : reward.type , ItemCount : reward.itemCount } ) ;
2025-02-25 04:38:47 -08:00
}
2025-03-09 07:42:55 -07:00
if ( strippedItems ) {
for ( const si of strippedItems ) {
const droptable = ExportEnemies . droptables [ si . DropTable ] ;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if ( ! droptable ) {
logger . error ( ` unknown droptable ${ si . DropTable } ` ) ;
} else {
for ( let i = 0 ; i != si . DROP_MOD . length ; ++ i ) {
for ( const pool of droptable ) {
const reward = getRandomReward ( pool . items ) ! ;
logger . debug ( ` stripped droptable rolled ` , reward ) ;
await addItem ( inventory , reward . type ) ;
MissionRewards . push ( {
StoreItem : toStoreItem ( reward . type ) ,
ItemCount : 1 ,
FromEnemyCache : true // to show "identified"
} ) ;
}
}
}
}
}
2025-03-22 06:08:00 -07:00
if ( inventory . Nemesis ) {
if (
nemesis ||
( inventory . Nemesis . Faction == "FC_INFESTATION" &&
inventory . Nemesis . InfNodes . find ( obj = > obj . Node == rewardInfo . node ) )
) {
inventoryChanges . Nemesis ? ? = { } ;
const nodeIndex = inventory . Nemesis . InfNodes . findIndex ( obj = > obj . Node === rewardInfo . node ) ;
if ( nodeIndex !== - 1 ) inventory . Nemesis . InfNodes . splice ( nodeIndex , 1 ) ;
if ( inventory . Nemesis . InfNodes . length <= 0 ) {
if ( inventory . Nemesis . Faction != "FC_INFESTATION" ) {
inventory . Nemesis . Rank = Math . min ( inventory . Nemesis . Rank + 1 , 4 ) ;
inventoryChanges . Nemesis . Rank = inventory . Nemesis . Rank ;
}
inventory . Nemesis . InfNodes = getInfNodes ( inventory . Nemesis . Faction , inventory . Nemesis . Rank ) ;
}
if ( inventory . Nemesis . Faction == "FC_INFESTATION" ) {
inventoryChanges . Nemesis . HenchmenKilled ? ? = 0 ;
inventoryChanges . Nemesis . MissionCount ? ? = 0 ;
inventory . Nemesis . HenchmenKilled += 5 ;
inventory . Nemesis . MissionCount += 1 ;
inventoryChanges . Nemesis . HenchmenKilled += 5 ;
inventoryChanges . Nemesis . MissionCount += 1 ;
if ( inventory . Nemesis . HenchmenKilled >= 100 ) {
inventory . Nemesis . InfNodes = [
{
Node : "CrewBattleNode559" ,
Influence : 1
}
] ;
inventory . Nemesis . Weakened = true ;
inventoryChanges . Nemesis . Weakened = true ;
}
}
inventoryChanges . Nemesis . InfNodes = inventory . Nemesis . InfNodes ;
}
}
2025-02-06 07:11:31 -08:00
return { inventoryChanges , MissionRewards , credits } ;
2025-01-24 14:13:21 +01:00
} ;
2025-03-15 03:24:39 -07:00
interface IMissionCredits {
MissionCredits : number [ ] ;
CreditBonus : number [ ] ;
TotalCredits : number [ ] ;
DailyMissionBonus? : boolean ;
}
2025-02-24 08:50:07 -08:00
//creditBonus is not entirely accurate.
2025-01-24 14:13:21 +01:00
//TODO: consider ActiveBoosters
2025-02-06 07:11:31 -08:00
export const addCredits = (
2025-04-05 06:52:35 -07:00
inventory : TInventoryDatabaseDocument ,
2025-01-24 14:13:21 +01:00
{
missionDropCredits ,
missionCompletionCredits ,
2025-02-06 07:11:31 -08:00
rngRewardCredits
2025-01-24 14:13:21 +01:00
} : { missionDropCredits : number ; missionCompletionCredits : number ; rngRewardCredits : number }
2025-03-15 03:24:39 -07:00
) : IMissionCredits = > {
2025-01-24 14:13:21 +01:00
const hasDailyCreditBonus = true ;
const totalCredits = missionDropCredits + missionCompletionCredits + rngRewardCredits ;
2025-03-15 03:24:39 -07:00
const finalCredits : IMissionCredits = {
2025-01-24 14:13:21 +01:00
MissionCredits : [ missionDropCredits , missionDropCredits ] ,
CreditBonus : [ missionCompletionCredits , missionCompletionCredits ] ,
TotalCredits : [ totalCredits , totalCredits ]
} ;
2023-09-06 14:02:54 +04:00
2025-01-24 14:13:21 +01:00
if ( hasDailyCreditBonus ) {
2025-02-06 07:11:31 -08:00
inventory . RegularCredits += missionCompletionCredits ;
2025-01-24 14:13:21 +01:00
finalCredits . CreditBonus [ 1 ] *= 2 ;
finalCredits . MissionCredits [ 1 ] *= 2 ;
finalCredits . TotalCredits [ 1 ] *= 2 ;
}
if ( ! hasDailyCreditBonus ) {
return finalCredits ;
}
return { . . . finalCredits , DailyMissionBonus : true } ;
} ;
2025-02-24 08:50:07 -08:00
export const addFixedLevelRewards = (
rewards : IMissionRewardExternal ,
inventory : TInventoryDatabaseDocument ,
MissionRewards : IMissionReward [ ]
2025-03-15 03:24:39 -07:00
) : number = > {
2025-02-24 08:50:07 -08:00
let missionBonusCredits = 0 ;
if ( rewards . credits ) {
missionBonusCredits += rewards . credits ;
inventory . RegularCredits += rewards . credits ;
}
if ( rewards . items ) {
for ( const item of rewards . items ) {
MissionRewards . push ( {
2025-02-25 16:58:07 -08:00
StoreItem : item ,
2025-02-24 08:50:07 -08:00
ItemCount : 1
} ) ;
}
}
if ( rewards . countedItems ) {
for ( const item of rewards . countedItems ) {
MissionRewards . push ( {
2025-02-25 16:58:07 -08:00
StoreItem : ` /Lotus/StoreItems ${ item . ItemType . substring ( "Lotus/" . length ) } ` ,
2025-02-24 08:50:07 -08:00
ItemCount : item.ItemCount
} ) ;
}
}
if ( rewards . countedStoreItems ) {
for ( const item of rewards . countedStoreItems ) {
MissionRewards . push ( item ) ;
}
}
2025-04-06 10:19:15 -07:00
if ( rewards . droptable ) {
if ( rewards . droptable in ExportRewards ) {
logger . debug ( ` rolling ${ rewards . droptable } for level key rewards ` ) ;
const reward = getRandomRewardByChance ( ExportRewards [ rewards . droptable ] [ 0 ] ) ;
if ( reward ) {
MissionRewards . push ( {
StoreItem : reward.type ,
ItemCount : reward.itemCount
} ) ;
}
} else {
logger . error ( ` unknown droptable ${ rewards . droptable } ` ) ;
}
}
2025-02-24 08:50:07 -08:00
return missionBonusCredits ;
} ;
2025-03-31 04:15:00 -07:00
function getLevelCreditRewards ( node : IRegion ) : number {
const minEnemyLevel = node . minEnemyLevel ;
2025-01-24 14:13:21 +01:00
return 1000 + ( minEnemyLevel - 1 ) * 100 ;
//TODO: get dark sektor fixed credit rewards and railjack bonus
}
2025-02-25 04:42:49 -08:00
function getRandomMissionDrops ( RewardInfo : IRewardInfo ) : IMissionReward [ ] {
const drops : IMissionReward [ ] = [ ] ;
2025-02-06 07:11:31 -08:00
if ( RewardInfo . node in ExportRegions ) {
2024-06-23 15:12:32 +02:00
const region = ExportRegions [ RewardInfo . node ] ;
2025-02-25 04:36:10 -08:00
const rewardManifests : string [ ] =
RewardInfo . periodicMissionTag == "EliteAlert" || RewardInfo . periodicMissionTag == "EliteAlertB"
? [ "/Lotus/Types/Game/MissionDecks/EliteAlertMissionRewards/EliteAlertMissionRewards" ]
: region . rewardManifests ;
2023-09-06 14:02:54 +04:00
2024-06-23 15:12:32 +02:00
let rotations : number [ ] = [ ] ;
if ( RewardInfo . VaultsCracked ) {
// For Spy missions, e.g. 3 vaults cracked = A, B, C
for ( let i = 0 ; i != RewardInfo . VaultsCracked ; ++ i ) {
rotations . push ( i ) ;
}
} else {
const rotationCount = RewardInfo . rewardQualifications ? . length || 0 ;
rotations = getRotations ( rotationCount ) ;
2024-06-22 23:19:42 +02:00
}
2024-06-23 15:12:32 +02:00
rewardManifests
. map ( name = > ExportRewards [ name ] )
. forEach ( table = > {
for ( const rotation of rotations ) {
const rotationRewards = table [ rotation ] ;
const drop = getRandomRewardByChance ( rotationRewards ) ;
if ( drop ) {
2025-02-25 04:42:49 -08:00
drops . push ( { StoreItem : drop.type , ItemCount : drop.itemCount } ) ;
2024-06-23 15:12:32 +02:00
}
}
} ) ;
if ( region . cacheRewardManifest && RewardInfo . EnemyCachesFound ) {
const deck = ExportRewards [ region . cacheRewardManifest ] ;
for ( let rotation = 0 ; rotation != RewardInfo . EnemyCachesFound ; ++ rotation ) {
const drop = getRandomRewardByChance ( deck [ rotation ] ) ;
2024-06-22 02:39:29 +02:00
if ( drop ) {
2025-02-25 04:42:49 -08:00
drops . push ( { StoreItem : drop.type , ItemCount : drop.itemCount , FromEnemyCache : true } ) ;
2024-06-22 02:39:29 +02:00
}
2023-09-06 14:02:54 +04:00
}
2024-06-23 15:12:32 +02:00
}
2025-02-08 17:41:21 -08:00
if ( RewardInfo . nightmareMode ) {
const deck = ExportRewards [ "/Lotus/Types/Game/MissionDecks/NightmareModeRewards" ] ;
let rotation = 0 ;
if ( region . missionIndex === 3 && RewardInfo . rewardTier ) {
rotation = RewardInfo . rewardTier ;
} else if ( [ 6 , 7 , 8 , 10 , 11 ] . includes ( region . systemIndex ) ) {
rotation = 2 ;
} else if ( [ 4 , 9 , 12 , 14 , 15 , 16 , 17 , 18 ] . includes ( region . systemIndex ) ) {
rotation = 1 ;
}
const drop = getRandomRewardByChance ( deck [ rotation ] ) ;
if ( drop ) {
2025-02-25 04:42:49 -08:00
drops . push ( { StoreItem : drop.type , ItemCount : drop.itemCount } ) ;
2025-02-08 17:41:21 -08:00
}
}
2024-06-23 15:12:32 +02:00
}
2025-01-24 14:13:21 +01:00
return drops ;
}
2025-02-28 12:36:01 -08:00
const corruptedMods = [
"/Lotus/StoreItems/Upgrades/Mods/Melee/DualStat/CorruptedHeavyDamageChargeSpeedMod" , // Corrupt Charge
"/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/CorruptedCritDamagePistol" , // Hollow Point
"/Lotus/StoreItems/Upgrades/Mods/Melee/DualStat/CorruptedDamageSpeedMod" , // Spoiled Strike
"/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/CorruptedDamageRecoilPistol" , // Magnum Force
"/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/CorruptedMaxClipReloadSpeedPistol" , // Tainted Clip
"/Lotus/StoreItems/Upgrades/Mods/Rifle/DualStat/CorruptedCritRateFireRateRifle" , // Critical Delay
"/Lotus/StoreItems/Upgrades/Mods/Rifle/DualStat/CorruptedDamageRecoilRifle" , // Heavy Caliber
"/Lotus/StoreItems/Upgrades/Mods/Rifle/DualStat/CorruptedMaxClipReloadSpeedRifle" , // Tainted Mag
"/Lotus/StoreItems/Upgrades/Mods/Rifle/DualStat/CorruptedRecoilFireRateRifle" , // Vile Precision
"/Lotus/StoreItems/Upgrades/Mods/Warframe/DualStat/CorruptedDurationRangeWarframe" , // Narrow Minded
"/Lotus/StoreItems/Upgrades/Mods/Warframe/DualStat/CorruptedEfficiencyDurationWarframe" , // Fleeting Expertise
"/Lotus/StoreItems/Upgrades/Mods/Warframe/DualStat/CorruptedPowerEfficiencyWarframe" , // Blind Rage
"/Lotus/StoreItems/Upgrades/Mods/Warframe/DualStat/CorruptedRangePowerWarframe" , // Overextended
"/Lotus/StoreItems/Upgrades/Mods/Shotgun/DualStat/CorruptedAccuracyFireRateShotgun" , // Tainted Shell
"/Lotus/StoreItems/Upgrades/Mods/Shotgun/DualStat/CorruptedDamageAccuracyShotgun" , // Vicious Spread
"/Lotus/StoreItems/Upgrades/Mods/Shotgun/DualStat/CorruptedMaxClipReloadSpeedShotgun" , // Burdened Magazine
"/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/CorruptedFireRateDamagePistol" , // Anemic Agility
"/Lotus/StoreItems/Upgrades/Mods/Rifle/DualStat/CorruptedFireRateDamageRifle" , // Vile Acceleration
"/Lotus/StoreItems/Upgrades/Mods/Shotgun/DualStat/CorruptedFireRateDamageShotgun" , // Frail Momentum
"/Lotus/StoreItems/Upgrades/Mods/Shotgun/DualStat/CorruptedCritChanceFireRateShotgun" , // Critical Deceleration
"/Lotus/StoreItems/Upgrades/Mods/Pistol/DualStat/CorruptedCritChanceFireRatePistol" , // Creeping Bullseye
"/Lotus/StoreItems/Upgrades/Mods/Warframe/DualStat/CorruptedPowerStrengthPowerDurationWarframe" , // Transient Fortitude
"/Lotus/StoreItems/Upgrades/Mods/Rifle/DualStat/CorruptedReloadSpeedMaxClipRifle" , // Depleted Reload
"/Lotus/StoreItems/Upgrades/Mods/Warframe/DualStat/FixedShieldAndShieldGatingDuration" // Catalyzing Shields
] ;
2025-03-07 00:40:22 -08:00
const libraryPersonalTargetToAvatar : Record < string , string > = {
"/Lotus/Types/Game/Library/Targets/DragonframeQuestTarget" :
"/Lotus/Types/Enemies/Grineer/Desert/Avatars/RifleLancerAvatar" ,
"/Lotus/Types/Game/Library/Targets/Research1Target" :
"/Lotus/Types/Enemies/Grineer/Desert/Avatars/RifleLancerAvatar" ,
"/Lotus/Types/Game/Library/Targets/Research2Target" :
"/Lotus/Types/Enemies/Corpus/BipedRobot/AIWeek/LaserDiscBipedAvatar" ,
"/Lotus/Types/Game/Library/Targets/Research3Target" :
"/Lotus/Types/Enemies/Grineer/Desert/Avatars/EvisceratorLancerAvatar" ,
"/Lotus/Types/Game/Library/Targets/Research4Target" : "/Lotus/Types/Enemies/Orokin/OrokinHealingAncientAvatar" ,
"/Lotus/Types/Game/Library/Targets/Research5Target" :
"/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/ShotgunSpacemanAvatar" ,
"/Lotus/Types/Game/Library/Targets/Research6Target" : "/Lotus/Types/Enemies/Infested/AiWeek/Runners/RunnerAvatar" ,
"/Lotus/Types/Game/Library/Targets/Research7Target" :
"/Lotus/Types/Enemies/Grineer/AIWeek/Avatars/GrineerMeleeStaffAvatar" ,
"/Lotus/Types/Game/Library/Targets/Research8Target" : "/Lotus/Types/Enemies/Orokin/OrokinHeavyFemaleAvatar" ,
"/Lotus/Types/Game/Library/Targets/Research9Target" :
"/Lotus/Types/Enemies/Infested/AiWeek/Quadrupeds/QuadrupedAvatar" ,
"/Lotus/Types/Game/Library/Targets/Research10Target" :
"/Lotus/Types/Enemies/Corpus/Spaceman/AIWeek/NullifySpacemanAvatar"
} ;