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-04-15 09:46:08 -07:00
import { IRngResult , SRng , 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-04-23 11:37:10 -07:00
addFusionPoints ,
2025-01-24 14:13:21 +01:00
addFusionTreasures ,
addGearExpByCategory ,
2025-03-09 07:42:55 -07:00
addItem ,
2025-04-07 05:29:32 -07:00
addLevelKeys ,
2025-04-15 06:16:19 -07:00
addLoreFragmentScans ,
2025-01-24 14:13:21 +01:00
addMiscItems ,
addMissionComplete ,
addMods ,
addRecipes ,
2025-03-22 01:15:09 -07:00
addShipDecorations ,
2025-04-12 06:13:44 -07:00
addStanding ,
2025-01-24 14:13:21 +01:00
combineInventoryChanges ,
2025-04-15 09:46:08 -07:00
generateRewardSeed ,
2025-04-25 11:53:34 -07:00
getCalendarProgress ,
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-04-12 06:13:44 -07:00
import { IAffiliationMods , 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-04-23 11:35:57 -07:00
import { IMissionCredits , 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-04-19 09:06:38 -07:00
import { getLiteSortie , getWorldState , idToWeek } from "./worldStateService" ;
2025-04-17 08:01:59 -07:00
import { config } from "./configService" ;
2025-04-22 09:59:41 -07:00
import libraryDailyTasks from "@/static/fixed_responses/libraryDailyTasks.json" ;
2025-01-24 14:13:21 +01:00
2025-04-15 14:58:15 -07:00
const getRotations = ( rewardInfo : IRewardInfo , tierOverride? : number ) : number [ ] = > {
2025-04-15 10:47:38 -07:00
// For Spy missions, e.g. 3 vaults cracked = A, B, C
if ( rewardInfo . VaultsCracked ) {
const rotations : number [ ] = [ ] ;
for ( let i = 0 ; i != rewardInfo . VaultsCracked ; ++ i ) {
rotations . push ( i ) ;
}
return rotations ;
}
// For Rescue missions
2025-04-16 08:36:00 -07:00
if ( rewardInfo . node in ExportRegions && ExportRegions [ rewardInfo . node ] . missionIndex == 3 && rewardInfo . rewardTier ) {
2025-04-15 10:47:38 -07:00
return [ rewardInfo . rewardTier ] ;
}
2025-04-25 11:52:16 -07:00
const rotationCount = rewardInfo . rewardQualifications ? . length || 0 ;
2025-04-20 07:53:11 -07:00
2025-04-25 11:52:16 -07:00
// Empty or absent rewardQualifications should not give rewards:
// - Aborting a railjack mission (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1741)
// - Completing only 1 zone of (E)SO (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1823)
2025-01-24 14:13:21 +01:00
2025-04-10 12:54:29 -07:00
const rotationPattern =
tierOverride === undefined
? [ 0 , 0 , 1 , 2 ] // A, A, B, C
: [ tierOverride ] ;
2025-01-24 14:13:21 +01:00
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-04-15 09:46:08 -07:00
const getRandomRewardByChance = ( pool : IReward [ ] , rng? : SRng ) : IRngResult | undefined = > {
if ( rng ) {
const res = rng . randomReward ( pool as IRngResult [ ] ) ;
rng . randomFloat ( ) ; // something related to rewards multiplier
return res ;
}
2025-01-24 14:13:21 +01:00
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-07 05:29:32 -07:00
if ( inventoryUpdates . EndOfMatchUpload ) {
if ( 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
}
] ) ;
}
}
if ( inventoryUpdates . KeyToRemove ) {
if ( ! inventoryUpdates . KeyOwner || inventory . accountOwnerId . equals ( inventoryUpdates . KeyOwner ) ) {
addLevelKeys ( inventory , [
{
ItemType : inventoryUpdates.KeyToRemove ,
ItemCount : - 1
}
] ) ;
}
}
2025-04-17 10:58:40 -07:00
// Somewhat heuristically detect G3 capture:
// - https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1365
// - https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1694
2025-04-19 09:04:04 -07:00
// - https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1724
2025-04-07 05:29:32 -07:00
if (
inventoryUpdates . MissionFailed &&
inventoryUpdates . MissionStatus == "GS_FAILURE" &&
inventoryUpdates . ObjectiveReached &&
2025-04-17 10:58:40 -07:00
! inventoryUpdates . LockedWeaponGroup &&
2025-04-19 09:04:04 -07:00
! inventory . LockedWeaponGroup &&
2025-04-17 10:58:40 -07:00
! inventoryUpdates . LevelKeyName
2025-04-07 05:29:32 -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 ) ;
await createMessage ( inventory . accountOwnerId , [
{
sndr : "/Lotus/Language/Menu/Mailbox_WarframeSender" ,
msg : "/Lotus/Language/G1Quests/BrandedMessage" ,
sub : "/Lotus/Language/G1Quests/BrandedTitle" ,
att : [ "/Lotus/Types/Recipes/Components/BrandRemovalBlueprint" ] ,
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-04-04 06:02:55 -07:00
}
}
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-04-10 07:15:54 -07:00
if ( inventoryUpdates . MissionStatus == "GS_SUCCESS" && inventoryUpdates . RewardInfo . jobId ) {
// e.g. for Profit-Taker Phase 1:
// JobTier: -6,
// jobId: '/Lotus/Types/Gameplay/Venus/Jobs/Heists/HeistProfitTakerBountyOne_-6_SolarisUnitedHub1_663a71c80000000000000025_EudicoHeists',
// This is sent multiple times, with JobStage starting at 0 and incrementing each time, but only the final upload has GS_SUCCESS.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [ bounty , tier , hub , id , tag ] = inventoryUpdates . RewardInfo . jobId . split ( "_" ) ;
if ( tag == "EudicoHeists" ) {
inventory . CompletedJobChains ? ? = [ ] ;
let chain = inventory . CompletedJobChains . find ( x = > x . LocationTag == tag ) ;
if ( ! chain ) {
chain =
inventory . CompletedJobChains [
inventory . CompletedJobChains . push ( { LocationTag : tag , Jobs : [ ] } ) - 1
] ;
}
if ( ! chain . Jobs . includes ( bounty ) ) {
chain . Jobs . push ( bounty ) ;
2025-04-11 06:54:59 -07:00
if ( bounty == "/Lotus/Types/Gameplay/Venus/Jobs/Heists/HeistProfitTakerBountyThree" ) {
await createMessage ( inventory . accountOwnerId , [
{
sub : "/Lotus/Language/SolarisHeists/HeavyCatalystInboxTitle" ,
sndr : "/Lotus/Language/Bosses/Ordis" ,
msg : "/Lotus/Language/SolarisHeists/HeavyCatalystInboxMessage" ,
icon : "/Lotus/Interface/Icons/Npcs/Ordis.png" ,
att : [ "/Lotus/Types/Restoratives/HeavyWeaponSummon" ] ,
highPriority : true
}
] ) ;
await addItem ( inventory , "/Lotus/Types/Items/MiscItems/HeavyWeaponCatalyst" , 1 ) ;
}
2025-04-10 07:15:54 -07:00
}
}
}
2025-03-08 05:36:06 -08: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" :
2025-04-08 03:06:36 -07:00
addChallenges ( inventory , value , inventoryUpdates . SeasonChallengeCompletions ) ;
2025-01-24 14:13:21 +01:00
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" : {
2025-04-23 11:37:10 -07:00
let fusionPointsDelta = 0 ;
2025-01-24 14:13:21 +01:00
for ( const fusionBundle of value ) {
2025-04-23 11:37:10 -07:00
fusionPointsDelta += addFusionPoints (
inventory ,
ExportFusionBundles [ fusionBundle . ItemType ] . fusionPoints * fusionBundle . ItemCount
) ;
2025-01-24 14:13:21 +01:00
}
2025-04-23 11:37:10 -07:00
inventoryChanges . FusionPoints = fusionPointsDelta ;
2025-01-24 14:13:21 +01:00
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-04-15 06:16:19 -07:00
addLoreFragmentScans ( inventory , value ) ;
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 ;
2025-04-22 09:59:41 -07:00
if ( inventory . LibraryPersonalTarget ) {
const taskAvatar = libraryPersonalTargetToAvatar [ inventory . LibraryPersonalTarget ] ;
const taskAvatars = libraryDailyTasks . find ( x = > x . indexOf ( taskAvatar ) != - 1 ) ! ;
if ( taskAvatars . indexOf ( scan . EnemyType ) != - 1 ) {
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
] ;
}
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 ;
2025-03-07 00:40:22 -08:00
}
}
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" : {
2025-04-18 11:23:52 -07:00
if ( inventory . CompletedSorties . indexOf ( value ) == - 1 ) {
inventory . CompletedSorties . push ( value ) ;
}
2025-02-09 09:39:45 -08:00
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" : {
2025-04-17 08:01:59 -07:00
if ( ! config . noDeathMarks ) {
for ( const bossName of value ) {
if ( inventory . DeathMarks . indexOf ( bossName ) == - 1 ) {
// It's a new death mark; we have to say the line.
await createMessage ( inventory . accountOwnerId , [
{
sub : bossName ,
sndr : "/Lotus/Language/G1Quests/DeathMarkSender" ,
msg : "/Lotus/Language/G1Quests/DeathMarkMessage" ,
icon : "/Lotus/Interface/Icons/Npcs/Stalker_d.png" ,
highPriority : true ,
expiry : new Date ( Date . now ( ) + 86400 _000 ) // TOVERIFY: This type of inbox message seems to automatically delete itself. We'll just delete it after 24 hours, but it's clear if this is correct.
}
] ) ;
}
2025-03-16 04:33:21 -07:00
}
2025-04-17 08:01:59 -07:00
inventory . DeathMarks = value ;
2025-03-16 04:33:21 -07:00
}
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-04-18 11:27:29 -07:00
case "InvasionProgress" : {
for ( const clientProgress of value ) {
const dbProgress = inventory . QualifyingInvasions . find ( x = >
x . invasionId . equals ( clientProgress . _id . $oid )
) ;
if ( dbProgress ) {
dbProgress . Delta += clientProgress . Delta ;
dbProgress . AttackerScore += clientProgress . AttackerScore ;
dbProgress . DefenderScore += clientProgress . DefenderScore ;
} else {
inventory . QualifyingInvasions . push ( {
invasionId : new Types . ObjectId ( clientProgress . _id . $oid ) ,
Delta : clientProgress.Delta ,
AttackerScore : clientProgress.AttackerScore ,
DefenderScore : clientProgress.DefenderScore
} ) ;
}
}
break ;
}
2025-04-25 11:53:34 -07:00
case "CalendarProgress" : {
const calendarProgress = getCalendarProgress ( inventory ) ;
for ( const progress of value ) {
const challengeName = progress . challenge . substring ( progress . challenge . lastIndexOf ( "/" ) + 1 ) ;
calendarProgress . SeasonProgress . LastCompletedChallengeDayIdx ++ ;
calendarProgress . SeasonProgress . ActivatedChallenges . push ( challengeName ) ;
}
break ;
}
2025-04-25 11:56:40 -07:00
case "duviriCaveOffers" : {
// Duviri cave offers (generated with the duviri seed) change after completing one of its game modes (not when aborting).
if ( inventoryUpdates . MissionStatus != "GS_QUIT" ) {
inventory . DuviriInfo . Seed = generateRewardSeed ( ) ;
}
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-04-12 06:13:44 -07:00
AffiliationMods? : IAffiliationMods [ ] ;
SyndicateXPItemReward? : number ;
2025-04-23 11:35:57 -07:00
ConquestCompletedMissionsCount? : number ;
2025-03-15 03:24:39 -07:00
}
2025-04-23 11:35:57 -07:00
interface IConquestReward {
at : number ;
pool : IRngResult [ ] ;
}
const labConquestRewards : IConquestReward [ ] = [
{
at : 5 ,
pool : ExportRewards [
"/Lotus/Types/Game/MissionDecks/EntratiLabConquestRewards/EntratiLabConquestSilverRewards"
] [ 0 ] as IRngResult [ ]
} ,
{
at : 10 ,
pool : ExportRewards [
"/Lotus/Types/Game/MissionDecks/EntratiLabConquestRewards/EntratiLabConquestSilverRewards"
] [ 0 ] as IRngResult [ ]
} ,
{
at : 15 ,
pool : [
{
type : "/Lotus/StoreItems/Types/Gameplay/EntratiLab/Resources/EntratiLanthornBundle" ,
itemCount : 3 ,
probability : 1
}
]
} ,
{
at : 20 ,
pool : ExportRewards [
"/Lotus/Types/Game/MissionDecks/EntratiLabConquestRewards/EntratiLabConquestGoldRewards"
] [ 0 ] as IRngResult [ ]
} ,
{
at : 28 ,
pool : [
{
type : "/Lotus/StoreItems/Types/Items/MiscItems/DistillPoints" ,
itemCount : 20 ,
probability : 1
}
]
} ,
{
at : 31 ,
pool : ExportRewards [
"/Lotus/Types/Game/MissionDecks/EntratiLabConquestRewards/EntratiLabConquestGoldRewards"
] [ 0 ] as IRngResult [ ]
} ,
{
at : 34 ,
pool : ExportRewards [
"/Lotus/Types/Game/MissionDecks/EntratiLabConquestRewards/EntratiLabConquestArcaneRewards"
] [ 0 ] as IRngResult [ ]
} ,
{
at : 37 ,
pool : [
{
type : "/Lotus/StoreItems/Types/Items/MiscItems/DistillPoints" ,
itemCount : 50 ,
probability : 1
}
]
}
] ;
const hexConquestRewards : IConquestReward [ ] = [
{
at : 5 ,
pool : ExportRewards [
"/Lotus/Types/Game/MissionDecks/1999ConquestRewards/1999ConquestSilverRewards"
] [ 0 ] as IRngResult [ ]
} ,
{
at : 10 ,
pool : ExportRewards [
"/Lotus/Types/Game/MissionDecks/1999ConquestRewards/1999ConquestSilverRewards"
] [ 0 ] as IRngResult [ ]
} ,
{
at : 15 ,
pool : [
{
type : "/Lotus/StoreItems/Types/BoosterPacks/1999StickersPackEchoesArchimedea" ,
itemCount : 1 ,
probability : 1
}
]
} ,
{
at : 20 ,
pool : ExportRewards [
"/Lotus/Types/Game/MissionDecks/1999ConquestRewards/1999ConquestGoldRewards"
] [ 0 ] as IRngResult [ ]
} ,
{
at : 28 ,
pool : [
{
type : "/Lotus/StoreItems/Types/Items/MiscItems/1999ConquestBucks" ,
itemCount : 6 ,
probability : 1
}
]
} ,
{
at : 31 ,
pool : ExportRewards [
"/Lotus/Types/Game/MissionDecks/1999ConquestRewards/1999ConquestGoldRewards"
] [ 0 ] as IRngResult [ ]
} ,
{
at : 34 ,
pool : ExportRewards [
"/Lotus/Types/Game/MissionDecks/1999ConquestRewards/1999ConquestArcaneRewards"
] [ 0 ] as IRngResult [ ]
} ,
{
at : 37 ,
pool : [
{
type : "/Lotus/StoreItems/Types/Items/MiscItems/1999ConquestBucks" ,
itemCount : 9 ,
probability : 1
}
]
}
] ;
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-04-10 12:54:29 -07:00
wagerTier : wagerTier ,
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-04-18 11:23:52 -07:00
} : IMissionInventoryUpdateRequest ,
firstCompletion : boolean
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-04-18 11:23:52 -07:00
const MissionRewards : IMissionReward [ ] = getRandomMissionDrops ( inventory , rewardInfo , wagerTier , firstCompletion ) ;
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 = { } ;
2025-04-12 06:13:44 -07:00
const AffiliationMods : IAffiliationMods [ ] = [ ] ;
let SyndicateXPItemReward ;
2025-04-23 11:35:57 -07:00
let ConquestCompletedMissionsCount ;
2025-01-24 14:13:21 +01:00
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 ) {
2025-04-15 14:58:15 -07:00
addFixedLevelRewards ( fixedLevelRewards . levelKeyRewards , inventory , MissionRewards , rewardInfo ) ;
2025-02-24 08:50:07 -08:00
}
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
2025-04-15 06:16:31 -07:00
if (
node . missionIndex != 23 && // junction
node . missionIndex != 28 && // open world
missions . Tag != "SolNode761" && // the index
missions . Tag != "SolNode762" && // the index
missions . Tag != "SolNode763" && // the index
2025-04-25 11:52:16 -07:00
missions . Tag != "CrewBattleNode556" && // free flight
getRotations ( rewardInfo ) . length > 0 // (E)SO should not give credits for only completing zone 1, in which case it has no rewardQualifications (https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1823)
2025-04-15 06:16:31 -07:00
) {
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 ) {
2025-04-15 14:58:15 -07:00
missionCompletionCredits += addFixedLevelRewards ( node . missionReward , inventory , MissionRewards , rewardInfo ) ;
2025-02-21 06:32:05 -08:00
}
2025-04-16 06:30:06 -07:00
2025-04-25 11:52:42 -07:00
if ( rewardInfo . sortieTag == "Mission1" ) {
missionCompletionCredits += 20 _000 ;
} else if ( rewardInfo . sortieTag == "Mission2" ) {
missionCompletionCredits += 30 _000 ;
} else if ( rewardInfo . sortieTag == "Final" ) {
missionCompletionCredits += 50 _000 ;
}
2025-04-16 06:30:06 -07:00
if ( missions . Tag == "PlutoToErisJunction" ) {
await createMessage ( inventory . accountOwnerId , [
{
sndr : "/Lotus/Language/G1Quests/GolemQuestJordasName" ,
msg : "/Lotus/Language/G1Quests/GolemQuestIntroBody" ,
att : [ "/Lotus/Types/Keys/GolemQuest/GolemQuestKeyChainItem" ] ,
sub : "/Lotus/Language/G1Quests/GolemQuestIntroTitle" ,
icon : "/Lotus/Interface/Icons/Npcs/JordasPortrait.png" ,
highPriority : true
}
] ) ;
}
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-04-23 11:35:57 -07:00
if ( rewardInfo . ConquestCompleted !== undefined ) {
let score = 1 ;
if ( rewardInfo . ConquestHardModeActive === 1 ) score += 3 ;
if ( rewardInfo . ConquestPersonalModifiersActive !== undefined )
score += rewardInfo . ConquestPersonalModifiersActive ;
if ( rewardInfo . ConquestEquipmentSuggestionsFulfilled !== undefined )
score += rewardInfo . ConquestEquipmentSuggestionsFulfilled ;
score *= rewardInfo . ConquestCompleted + 1 ;
if ( rewardInfo . ConquestCompleted == 2 && rewardInfo . ConquestHardModeActive === 1 ) score += 1 ;
logger . debug ( ` completed conquest mission ${ rewardInfo . ConquestCompleted + 1 } for a score of ${ score } ` ) ;
const conquestType = rewardInfo . ConquestType ;
const conquestNode =
conquestType == "HexConquest" ? "EchoesHexConquestHardModeUnlocked" : "EntratiLabConquestHardModeUnlocked" ;
if ( score >= 25 && inventory . NodeIntrosCompleted . indexOf ( conquestNode ) == - 1 )
inventory . NodeIntrosCompleted . push ( conquestNode ) ;
if ( conquestType == "HexConquest" ) {
inventory . EchoesHexConquestCacheScoreMission ? ? = 0 ;
if ( score > inventory . EchoesHexConquestCacheScoreMission ) {
for ( const reward of hexConquestRewards ) {
if ( score >= reward . at && inventory . EchoesHexConquestCacheScoreMission < reward . at ) {
const rolled = getRandomReward ( reward . pool ) ! ;
logger . debug ( ` rolled hex conquest reward for reaching ${ reward . at } points ` , rolled ) ;
MissionRewards . push ( {
StoreItem : rolled.type ,
ItemCount : rolled.itemCount
} ) ;
}
}
inventory . EchoesHexConquestCacheScoreMission = score ;
}
} else {
inventory . EntratiLabConquestCacheScoreMission ? ? = 0 ;
if ( score > inventory . EntratiLabConquestCacheScoreMission ) {
for ( const reward of labConquestRewards ) {
if ( score >= reward . at && inventory . EntratiLabConquestCacheScoreMission < reward . at ) {
const rolled = getRandomReward ( reward . pool ) ! ;
logger . debug ( ` rolled lab conquest reward for reaching ${ reward . at } points ` , rolled ) ;
MissionRewards . push ( {
StoreItem : rolled.type ,
ItemCount : rolled.itemCount
} ) ;
}
}
inventory . EntratiLabConquestCacheScoreMission = score ;
}
}
ConquestCompletedMissionsCount = rewardInfo . ConquestCompleted == 2 ? 0 : rewardInfo.ConquestCompleted + 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 ) {
2025-04-18 11:16:58 -07:00
if ( si . DropTable == "/Lotus/Types/DropTables/ManInTheWall/MITWGruzzlingArcanesDropTable" ) {
logger . debug (
` rewriting ${ si . DropTable } to /Lotus/Types/DropTables/EntratiLabDropTables/DoppelgangerDropTable `
) ;
si . DropTable = "/Lotus/Types/DropTables/EntratiLabDropTables/DoppelgangerDropTable" ;
}
2025-04-10 12:40:57 -07:00
const droptables = ExportEnemies . droptables [ si . DropTable ] ? ? [ ] ;
if ( si . DROP_MOD ) {
const modDroptable = droptables . find ( x = > x . type == "mod" ) ;
if ( modDroptable ) {
2025-04-08 03:06:47 -07:00
for ( let i = 0 ; i != si . DROP_MOD . length ; ++ i ) {
2025-04-10 12:40:57 -07:00
const reward = getRandomReward ( modDroptable . items ) ! ;
2025-04-08 03:06:47 -07:00
logger . debug ( ` stripped droptable (mods pool) rolled ` , reward ) ;
await addItem ( inventory , reward . type ) ;
MissionRewards . push ( {
StoreItem : toStoreItem ( reward . type ) ,
ItemCount : 1 ,
FromEnemyCache : true // to show "identified"
} ) ;
}
2025-04-10 12:40:57 -07:00
} else {
logger . error ( ` unknown droptable ${ si . DropTable } for DROP_MOD ` ) ;
2025-04-08 03:06:47 -07:00
}
2025-04-10 12:40:57 -07:00
}
if ( si . DROP_BLUEPRINT ) {
const blueprintDroptable = droptables . find ( x = > x . type == "blueprint" ) ;
if ( blueprintDroptable ) {
2025-04-08 03:06:47 -07:00
for ( let i = 0 ; i != si . DROP_BLUEPRINT . length ; ++ i ) {
2025-04-10 12:40:57 -07:00
const reward = getRandomReward ( blueprintDroptable . items ) ! ;
2025-04-08 03:06:47 -07:00
logger . debug ( ` stripped droptable (blueprints pool) rolled ` , reward ) ;
2025-03-09 07:42:55 -07:00
await addItem ( inventory , reward . type ) ;
MissionRewards . push ( {
StoreItem : toStoreItem ( reward . type ) ,
ItemCount : 1 ,
FromEnemyCache : true // to show "identified"
} ) ;
}
2025-04-10 12:40:57 -07:00
} else {
logger . error ( ` unknown droptable ${ si . DropTable } for DROP_BLUEPRINT ` ) ;
2025-03-09 07:42:55 -07:00
}
}
}
}
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" ) {
inventory . Nemesis . MissionCount += 1 ;
2025-04-13 05:50:51 -07:00
inventoryChanges . Nemesis . MissionCount ? ? = 0 ;
2025-03-22 06:08:00 -07:00
inventoryChanges . Nemesis . MissionCount += 1 ;
}
inventoryChanges . Nemesis . InfNodes = inventory . Nemesis . InfNodes ;
}
}
2025-04-12 06:13:44 -07:00
if ( rewardInfo . JobStage != undefined && rewardInfo . jobId ) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
2025-04-16 06:28:34 -07:00
const [ jobType , unkIndex , hubNode , syndicateId , locationTag ] = rewardInfo . jobId . split ( "_" ) ;
2025-04-12 06:13:44 -07:00
const worldState = getWorldState ( ) ;
let syndicateEntry = worldState . SyndicateMissions . find ( m = > m . _id . $oid === syndicateId ) ;
if ( ! syndicateEntry ) syndicateEntry = worldState . SyndicateMissions . find ( m = > m . Tag === syndicateId ) ; // Sometimes syndicateId can be tag
if ( syndicateEntry && syndicateEntry . Jobs ) {
2025-04-16 06:28:34 -07:00
let currentJob = syndicateEntry . Jobs [ rewardInfo . JobTier ! ] ;
2025-04-12 06:13:44 -07:00
if ( syndicateEntry . Tag === "EntratiSyndicate" ) {
const vault = syndicateEntry . Jobs . find ( j = > j . locationTag === locationTag ) ;
if ( vault ) currentJob = vault ;
let medallionAmount = currentJob . xpAmounts [ rewardInfo . JobStage ] ;
if (
[ "DeimosEndlessAreaDefenseBounty" , "DeimosEndlessExcavateBounty" , "DeimosEndlessPurifyBounty" ] . some (
ending = > jobType . endsWith ( ending )
)
) {
const endlessJob = syndicateEntry . Jobs . find ( j = > j . endless ) ;
if ( endlessJob ) {
const index = rewardInfo . JobStage % endlessJob . xpAmounts . length ;
2025-04-13 05:51:02 -07:00
const excess = Math . floor ( rewardInfo . JobStage / ( endlessJob . xpAmounts . length - 1 ) ) ;
2025-04-12 06:13:44 -07:00
medallionAmount = Math . floor ( endlessJob . xpAmounts [ index ] * ( 1 + 0.15000001 * excess ) ) ;
}
}
await addItem ( inventory , "/Lotus/Types/Items/Deimos/EntratiFragmentUncommonB" , medallionAmount ) ;
MissionRewards . push ( {
StoreItem : "/Lotus/StoreItems/Types/Items/Deimos/EntratiFragmentUncommonB" ,
ItemCount : medallionAmount
} ) ;
SyndicateXPItemReward = medallionAmount ;
} else {
2025-04-16 06:28:34 -07:00
if ( rewardInfo . JobTier ! >= 0 ) {
2025-04-12 06:13:44 -07:00
AffiliationMods . push (
addStanding ( inventory , syndicateEntry . Tag , currentJob . xpAmounts [ rewardInfo . JobStage ] )
) ;
} else {
if ( jobType . endsWith ( "Heists/HeistProfitTakerBountyOne" ) && rewardInfo . JobStage === 2 ) {
AffiliationMods . push ( addStanding ( inventory , syndicateEntry . Tag , 1000 ) ) ;
}
if ( jobType . endsWith ( "Hunts/AllTeralystsHunt" ) && rewardInfo . JobStage === 2 ) {
AffiliationMods . push ( addStanding ( inventory , syndicateEntry . Tag , 5000 ) ) ;
}
if (
[
"Hunts/TeralystHunt" ,
"Heists/HeistProfitTakerBountyTwo" ,
"Heists/HeistProfitTakerBountyThree" ,
"Heists/HeistProfitTakerBountyFour" ,
"Heists/HeistExploiterBountyOne"
] . some ( ending = > jobType . endsWith ( ending ) )
) {
AffiliationMods . push ( addStanding ( inventory , syndicateEntry . Tag , 1000 ) ) ;
}
}
}
}
}
if ( rewardInfo . challengeMissionId ) {
const [ syndicateTag , tierStr ] = rewardInfo . challengeMissionId . split ( "_" ) ; // TODO: third part in HexSyndicate jobs - Chemistry points
const tier = Number ( tierStr ) ;
const isSteelPath = missions ? . Tier ;
if ( syndicateTag === "ZarimanSyndicate" ) {
let medallionAmount = tier + 1 ;
if ( isSteelPath ) medallionAmount = Math . round ( medallionAmount * 1.5 ) ;
await addItem ( inventory , "/Lotus/Types/Gameplay/Zariman/Resources/ZarimanDogTagBounty" , medallionAmount ) ;
MissionRewards . push ( {
StoreItem : "/Lotus/StoreItems/Types/Gameplay/Zariman/Resources/ZarimanDogTagBounty" ,
ItemCount : medallionAmount
} ) ;
SyndicateXPItemReward = medallionAmount ;
} else {
let standingAmount = ( tier + 1 ) * 1000 ;
if ( tier > 5 ) standingAmount = 7500 ; // InfestedLichBounty
if ( isSteelPath ) standingAmount *= 1.5 ;
AffiliationMods . push ( addStanding ( inventory , syndicateTag , standingAmount ) ) ;
}
if ( isSteelPath ) {
await addItem ( inventory , "/Lotus/Types/Items/MiscItems/SteelEssence" , 1 ) ;
MissionRewards . push ( {
StoreItem : "/Lotus/StoreItems/Types/Items/MiscItems/SteelEssence" ,
ItemCount : 1
} ) ;
}
}
2025-04-23 11:35:57 -07:00
return {
inventoryChanges ,
MissionRewards ,
credits ,
AffiliationMods ,
SyndicateXPItemReward ,
ConquestCompletedMissionsCount
} ;
2025-01-24 14:13:21 +01:00
} ;
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 ,
2025-04-15 14:58:15 -07:00
MissionRewards : IMissionReward [ ] ,
rewardInfo? : IRewardInfo
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 ) {
2025-04-15 14:58:15 -07:00
const rotations : number [ ] = rewardInfo ? getRotations ( rewardInfo ) : [ 0 ] ;
logger . debug ( ` rolling ${ rewards . droptable } for level key rewards ` , { rotations } ) ;
for ( const tier of rotations ) {
const reward = getRandomRewardByChance ( ExportRewards [ rewards . droptable ] [ tier ] ) ;
if ( reward ) {
MissionRewards . push ( {
StoreItem : reward.type ,
ItemCount : reward.itemCount
} ) ;
}
2025-04-06 10:19:15 -07:00
}
} 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-04-18 11:23:52 -07:00
function getRandomMissionDrops (
inventory : TInventoryDatabaseDocument ,
RewardInfo : IRewardInfo ,
tierOverride : number | undefined ,
firstCompletion : boolean
) : IMissionReward [ ] {
2025-02-25 04:42:49 -08:00
const drops : IMissionReward [ ] = [ ] ;
2025-04-18 11:23:52 -07:00
if ( RewardInfo . sortieTag == "Final" && firstCompletion ) {
const arr = RewardInfo . sortieId ! . split ( "_" ) ;
let sortieId = arr [ 1 ] ;
if ( sortieId == "Lite" ) {
sortieId = arr [ 2 ] ;
2025-04-19 09:06:38 -07:00
const boss = getLiteSortie ( idToWeek ( sortieId ) ) . Boss ;
2025-04-18 11:23:52 -07:00
let crystalType = {
SORTIE_BOSS_AMAR : "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalAmar" ,
SORTIE_BOSS_NIRA : "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalNira" ,
SORTIE_BOSS_BOREAL : "/Lotus/StoreItems/Types/Gameplay/NarmerSorties/ArchonCrystalBoreal"
} [ boss ] ;
const attenTag = {
SORTIE_BOSS_AMAR : "NarmerSortieAmarCrystalRewards" ,
SORTIE_BOSS_NIRA : "NarmerSortieNiraCrystalRewards" ,
SORTIE_BOSS_BOREAL : "NarmerSortieBorealCrystalRewards"
} [ boss ] ;
const attenIndex = inventory . SortieRewardAttenuation ? . findIndex ( x = > x . Tag == attenTag ) ? ? - 1 ;
const mythicProbability =
0.2 + ( inventory . SortieRewardAttenuation ? . find ( x = > x . Tag == attenTag ) ? . Atten ? ? 0 ) ;
if ( Math . random ( ) < mythicProbability ) {
crystalType += "Mythic" ;
if ( attenIndex != - 1 ) {
inventory . SortieRewardAttenuation ! . splice ( attenIndex , 1 ) ;
}
} else {
if ( attenIndex == - 1 ) {
inventory . SortieRewardAttenuation ? ? = [ ] ;
inventory . SortieRewardAttenuation . push ( {
Tag : attenTag ,
Atten : 0.2
} ) ;
} else {
inventory . SortieRewardAttenuation ! [ attenIndex ] . Atten += 0.2 ;
}
}
drops . push ( { StoreItem : crystalType , ItemCount : 1 } ) ;
const drop = getRandomRewardByChance (
ExportRewards [ "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards" ] [ 0 ]
) ! ;
drops . push ( { StoreItem : drop.type , ItemCount : drop.itemCount } ) ;
inventory . LastLiteSortieReward = [
{
SortieId : new Types . ObjectId ( sortieId ) ,
StoreItem : drop.type ,
Manifest : "/Lotus/Types/Game/MissionDecks/ArchonSortieRewards"
}
] ;
} else {
const drop = getRandomRewardByChance ( ExportRewards [ "/Lotus/Types/Game/MissionDecks/SortieRewards" ] [ 0 ] ) ! ;
drops . push ( { StoreItem : drop.type , ItemCount : drop.itemCount } ) ;
inventory . LastSortieReward = [
{
SortieId : new Types . ObjectId ( sortieId ) ,
StoreItem : drop.type ,
Manifest : "/Lotus/Types/Game/MissionDecks/SortieRewards"
}
] ;
}
2025-04-17 08:02:13 -07:00
}
2025-04-15 06:16:40 -07:00
if ( RewardInfo . periodicMissionTag ? . startsWith ( "HardDaily" ) ) {
drops . push ( {
StoreItem : "/Lotus/StoreItems/Types/Items/MiscItems/SteelEssence" ,
ItemCount : 5
} ) ;
}
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-04-18 11:18:26 -07:00
let rewardManifests : string [ ] ;
if ( RewardInfo . periodicMissionTag == "EliteAlert" || RewardInfo . periodicMissionTag == "EliteAlertB" ) {
rewardManifests = [ "/Lotus/Types/Game/MissionDecks/EliteAlertMissionRewards/EliteAlertMissionRewards" ] ;
} else if ( RewardInfo . invasionId && region . missionIndex == 0 ) {
// Invasion assassination has Phorid has the boss who should drop Nyx parts
// TODO: Check that the invasion faction is indeed FC_INFESTATION once the Invasions in worldState are more dynamic
rewardManifests = [ "/Lotus/Types/Game/MissionDecks/BossMissionRewards/NyxRewards" ] ;
} else {
rewardManifests = region . rewardManifests ;
}
2023-09-06 14:02:54 +04:00
2024-06-23 15:12:32 +02:00
let rotations : number [ ] = [ ] ;
2025-04-11 06:54:35 -07:00
if ( RewardInfo . jobId ) {
2025-04-13 05:51:02 -07:00
if ( RewardInfo . JobStage ! >= 0 ) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
2025-04-16 06:28:34 -07:00
const [ jobType , unkIndex , hubNode , syndicateId , locationTag ] = RewardInfo . jobId . split ( "_" ) ;
2025-04-13 05:51:02 -07:00
let isEndlessJob = false ;
if ( syndicateId ) {
const worldState = getWorldState ( ) ;
let syndicateEntry = worldState . SyndicateMissions . find ( m = > m . _id . $oid === syndicateId ) ;
if ( ! syndicateEntry ) syndicateEntry = worldState . SyndicateMissions . find ( m = > m . Tag === syndicateId ) ;
if ( syndicateEntry && syndicateEntry . Jobs ) {
2025-04-16 06:28:34 -07:00
let job = syndicateEntry . Jobs [ RewardInfo . JobTier ! ] ;
2025-04-13 05:51:02 -07:00
if ( syndicateEntry . Tag === "EntratiSyndicate" ) {
const vault = syndicateEntry . Jobs . find ( j = > j . locationTag === locationTag ) ;
2025-04-15 06:10:25 -07:00
if ( vault && locationTag ) job = vault ;
2025-04-13 05:51:02 -07:00
// if (
// [
// "DeimosRuinsExterminateBounty",
// "DeimosRuinsEscortBounty",
// "DeimosRuinsMistBounty",
// "DeimosRuinsPurifyBounty",
// "DeimosRuinsSacBounty"
// ].some(ending => jobType.endsWith(ending))
// ) {
// job.rewards = "TODO"; // Droptable for Arcana Isolation Vault
// }
if (
[
"DeimosEndlessAreaDefenseBounty" ,
"DeimosEndlessExcavateBounty" ,
"DeimosEndlessPurifyBounty"
] . some ( ending = > jobType . endsWith ( ending ) )
) {
const endlessJob = syndicateEntry . Jobs . find ( j = > j . endless ) ;
if ( endlessJob ) {
isEndlessJob = true ;
job = endlessJob ;
const excess = Math . floor ( RewardInfo . JobStage ! / ( j o b . x p A m o u n t s . l e n g t h - 1 ) ) ;
const rotationIndexes = [ 0 , 0 , 1 , 2 ] ;
const rotationIndex = rotationIndexes [ excess % rotationIndexes . length ] ;
const dropTable = [
"/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierBTableARewards" ,
"/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierBTableBRewards" ,
"/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierBTableCRewards"
] ;
job . rewards = dropTable [ rotationIndex ] ;
}
}
} else if ( syndicateEntry . Tag === "SolarisSyndicate" ) {
if ( jobType . endsWith ( "Heists/HeistProfitTakerBountyOne" ) && RewardInfo . JobStage == 2 ) {
job = {
rewards :
"/Lotus/Types/Game/MissionDecks/HeistJobMissionRewards/HeistTierATableARewards" ,
masteryReq : 0 ,
minEnemyLevel : 40 ,
maxEnemyLevel : 60 ,
xpAmounts : [ 1000 ]
} ;
RewardInfo . Q = false ; // Just in case
} else {
const tierMap = {
Two : "B" ,
Three : "C" ,
Four : "D"
} ;
for ( const [ key , tier ] of Object . entries ( tierMap ) ) {
if ( jobType . endsWith ( ` Heists/HeistProfitTakerBounty ${ key } ` ) ) {
job = {
rewards : ` /Lotus/Types/Game/MissionDecks/HeistJobMissionRewards/HeistTier ${ tier } TableARewards ` ,
masteryReq : 0 ,
minEnemyLevel : 40 ,
maxEnemyLevel : 60 ,
xpAmounts : [ 1000 ]
} ;
RewardInfo . Q = false ; // Just in case
break ;
}
}
}
}
rewardManifests = [ job . rewards ] ;
rotations = [ RewardInfo . JobStage ! % ( job . xpAmounts . length - 1 ) ] ;
if (
RewardInfo . Q &&
( RewardInfo . JobStage === job . xpAmounts . length - 1 || job . isVault ) &&
! isEndlessJob
) {
2025-04-15 18:48:17 +02:00
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
2025-04-15 06:10:25 -07:00
if ( ExportRewards [ job . rewards ] ) {
rewardManifests . push ( job . rewards ) ;
rotations . push ( ExportRewards [ job . rewards ] . length - 1 ) ;
}
2025-04-13 05:51:02 -07:00
}
}
2025-04-11 06:54:35 -07:00
}
}
2025-04-13 05:51:02 -07:00
} else if ( RewardInfo . challengeMissionId ) {
const rewardTables : Record < string , string [ ] > = {
EntratiLabSyndicate : [
"/Lotus/Types/Game/MissionDecks/EntratiLabJobMissionReward/TierATableRewards" ,
"/Lotus/Types/Game/MissionDecks/EntratiLabJobMissionReward/TierBTableRewards" ,
"/Lotus/Types/Game/MissionDecks/EntratiLabJobMissionReward/TierCTableRewards" ,
"/Lotus/Types/Game/MissionDecks/EntratiLabJobMissionReward/TierDTableRewards" ,
"/Lotus/Types/Game/MissionDecks/EntratiLabJobMissionReward/TierETableRewards"
] ,
ZarimanSyndicate : [
"/Lotus/Types/Game/MissionDecks/ZarimanJobMissionRewards/TierATableRewards" ,
"/Lotus/Types/Game/MissionDecks/ZarimanJobMissionRewards/TierBTableRewards" ,
"/Lotus/Types/Game/MissionDecks/ZarimanJobMissionRewards/TierCTableRewards" ,
"/Lotus/Types/Game/MissionDecks/ZarimanJobMissionRewards/TierDTableRewards" ,
"/Lotus/Types/Game/MissionDecks/ZarimanJobMissionRewards/TierETableRewards"
] ,
HexSyndicate : [
"/Lotus/Types/Game/MissionDecks/1999MissionRewards/TierABountyRewards" ,
"/Lotus/Types/Game/MissionDecks/1999MissionRewards/TierBBountyRewards" ,
"/Lotus/Types/Game/MissionDecks/1999MissionRewards/TierCBountyRewards" ,
"/Lotus/Types/Game/MissionDecks/1999MissionRewards/TierDBountyRewards" ,
"/Lotus/Types/Game/MissionDecks/1999MissionRewards/TierEBountyRewards" ,
"/Lotus/Types/Game/MissionDecks/1999MissionRewards/TierFBountyRewards" ,
"/Lotus/Types/Game/MissionDecks/1999MissionRewards/InfestedLichBountyRewards"
]
} ;
const [ syndicateTag , tierStr ] = RewardInfo . challengeMissionId . split ( "_" ) ;
const tier = Number ( tierStr ) ;
const rewardTable = rewardTables [ syndicateTag ] [ tier ] ;
if ( rewardTable ) {
rewardManifests = [ rewardTable ] ;
rotations = [ 0 ] ;
} else {
logger . error ( ` Unknown syndicate or tier: ${ RewardInfo . challengeMissionId } ` ) ;
}
2024-06-23 15:12:32 +02:00
} else {
2025-04-15 10:47:38 -07:00
rotations = getRotations ( RewardInfo , tierOverride ) ;
2024-06-22 23:19:42 +02:00
}
2025-04-11 06:54:35 -07:00
if ( rewardManifests . length != 0 ) {
logger . debug ( ` generating random mission rewards ` , { rewardManifests , rotations } ) ;
}
2025-04-25 11:54:11 -07:00
if ( RewardInfo . rewardSeed ) {
if ( RewardInfo . rewardSeed != inventory . RewardSeed ) {
logger . warn ( ` RewardSeed mismatch: ` , { client : RewardInfo.rewardSeed , database : inventory.RewardSeed } ) ;
}
}
2025-04-15 09:46:08 -07:00
const rng = new SRng ( BigInt ( RewardInfo . rewardSeed ? ? generateRewardSeed ( ) ) ^ 0xffffffffffffffff n ) ;
2025-04-15 06:10:25 -07:00
rewardManifests . forEach ( name = > {
const table = ExportRewards [ name ] ;
2025-04-15 18:48:17 +02:00
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
2025-04-15 06:10:25 -07:00
if ( ! table ) {
logger . error ( ` unknown droptable: ${ name } ` ) ;
return ;
}
for ( const rotation of rotations ) {
const rotationRewards = table [ rotation ] ;
2025-04-15 09:46:08 -07:00
const drop = getRandomRewardByChance ( rotationRewards , rng ) ;
2025-04-15 06:10:25 -07:00
if ( drop ) {
drops . push ( { StoreItem : drop.type , ItemCount : drop.itemCount } ) ;
2024-06-23 15:12:32 +02:00
}
2025-04-15 06:10:25 -07:00
}
} ) ;
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
}
2025-04-16 06:30:36 -07:00
}
if ( RewardInfo . PurgatoryRewardQualifications ) {
for ( const encodedQualification of RewardInfo . PurgatoryRewardQualifications ) {
const qualification = parseInt ( encodedQualification ) - 1 ;
if ( qualification < 0 || qualification > 8 ) {
logger . error ( ` unexpected purgatory reward qualification: ${ qualification } ` ) ;
} else {
const drop = getRandomRewardByChance (
ExportRewards [
[
"/Lotus/Types/Game/MissionDecks/PurgatoryMissionRewards/PurgatoryBlackTokenRewards" ,
"/Lotus/Types/Game/MissionDecks/PurgatoryMissionRewards/PurgatoryGoldTokenRewards" ,
"/Lotus/Types/Game/MissionDecks/PurgatoryMissionRewards/PurgatoryBlueTokenRewards"
] [ Math . trunc ( qualification / 3 ) ]
] [ qualification % 3 ]
) ;
if ( drop ) {
drops . push ( {
StoreItem : drop.type ,
ItemCount : drop.itemCount ,
FromEnemyCache : true // to show "identified"
} ) ;
}
}
}
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"
} ;