2025-02-24 08:50:07 -08:00
import {
ExportFusionBundles ,
ExportRegions ,
ExportRewards ,
IMissionReward as IMissionRewardExternal ,
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" ;
2024-07-03 12:30:32 +02:00
import { IRngResult , getRandomReward } from "@/src/services/rngService" ;
2025-01-27 13:18:16 +01:00
import { equipmentKeys , IInventoryDatabase , TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes" ;
2025-01-24 14:13:21 +01:00
import {
addChallenges ,
addConsumables ,
2025-02-05 12:23:35 -08:00
addCrewShipAmmo ,
addCrewShipRawSalvage ,
2025-01-27 18:11:05 +01:00
addFocusXpIncreases ,
2025-01-24 14:13:21 +01:00
addFusionTreasures ,
addGearExpByCategory ,
addMiscItems ,
addMissionComplete ,
addMods ,
addRecipes ,
combineInventoryChanges ,
updateSyndicate
} from "@/src/services/inventoryService" ;
import { updateQuestKey } from "@/src/services/questService" ;
import { HydratedDocument } from "mongoose" ;
import { IInventoryChanges } from "@/src/types/purchaseTypes" ;
import { getLevelKeyRewards , getNode } from "@/src/services/itemDataService" ;
import { InventoryDocumentProps , TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel" ;
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-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
export const addMissionInventoryUpdates = (
inventory : HydratedDocument < IInventoryDatabase , InventoryDocumentProps > ,
inventoryUpdates : IMissionInventoryUpdateRequest
2025-02-24 20:56:34 -08:00
) : Partial < IInventoryDatabase > | undefined = > {
2025-01-24 14:13:21 +01:00
//TODO: type this properly
const inventoryChanges : Partial < IInventoryDatabase > = { } ;
if ( inventoryUpdates . MissionFailed === true ) {
return ;
2023-09-06 14:02:54 +04:00
}
2025-02-09 09:39:45 -08:00
if ( inventoryUpdates . RewardInfo && inventoryUpdates . RewardInfo . periodicMissionTag ) {
const tag = inventoryUpdates . RewardInfo . periodicMissionTag ;
const existingCompletion = inventory . PeriodicMissionCompletions . find ( completion = > completion . tag === tag ) ;
if ( existingCompletion ) {
existingCompletion . date = new Date ( ) ;
} else {
inventory . PeriodicMissionCompletions . push ( {
tag : tag ,
date : new Date ( )
} ) ;
}
}
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" :
updateQuestKey ( inventory , value ) ;
break ;
case "AffiliationChanges" :
updateSyndicate ( inventory , value ) ;
break ;
// Incarnon Challenges
case "EvolutionProgress" : {
for ( const evoProgress of value ) {
const entry = inventory . EvolutionProgress
? inventory . EvolutionProgress . find ( entry = > entry . ItemType == evoProgress . ItemType )
: undefined ;
if ( entry ) {
entry . Progress = evoProgress . Progress ;
entry . Rank = evoProgress . Rank ;
} else {
inventory . EvolutionProgress ? ? = [ ] ;
inventory . EvolutionProgress . push ( evoProgress ) ;
}
}
break ;
}
case "Missions" :
addMissionComplete ( inventory , value ) ;
break ;
case "LastRegionPlayed" :
inventory . LastRegionPlayed = value ;
break ;
case "RawUpgrades" :
addMods ( inventory , value ) ;
break ;
case "MiscItems" :
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-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-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 = > {
if ( inventory . LibraryActiveDailyTaskInfo ) {
if ( inventory . LibraryActiveDailyTaskInfo . EnemyTypes . find ( x = > x == scan . EnemyType ) ) {
inventory . LibraryActiveDailyTaskInfo . Scans ? ? = 0 ;
inventory . LibraryActiveDailyTaskInfo . Scans += scan . Count ;
} else {
logger . warn (
` ignoring synthesis of ${ scan . EnemyType } as it's not part of the active daily task `
) ;
}
} else {
logger . warn ( ` no library daily task active, ignoring synthesis of ${ scan . EnemyType } ` ) ;
}
} ) ;
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-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 ;
} ;
//TODO: return type of partial missioninventoryupdate response
export const addMissionRewards = async (
inventory : TInventoryDatabaseDocument ,
2025-02-06 07:11:31 -08:00
{
RewardInfo : rewardInfo ,
LevelKeyName : levelKeyName ,
Missions : missions ,
2025-02-25 04:38:47 -08:00
RegularCredits : creditDrops ,
VoidTearParticipantsCurrWave : voidTearWave
2025-02-06 07:11:31 -08:00
} : IMissionInventoryUpdateRequest
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-02-25 04:41:45 -08:00
if (
missions &&
missions . Tag != "" // #1013
) {
2025-02-24 08:50:07 -08:00
const node = getNode ( missions . Tag ) ;
//node based credit rewards for mission completion
if ( node . missionIndex !== 28 ) {
2025-02-24 20:56:34 -08:00
const levelCreditReward = getLevelCreditRewards ( missions . Tag ) ;
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-06 07:11:31 -08:00
for ( const reward of MissionRewards ) {
2025-02-23 03:54:46 -08:00
const inventoryChange = await handleStoreItemAcquisition ( reward . StoreItem , inventory , reward . ItemCount ) ;
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-02-26 15:42:13 -08:00
const reward = await crackRelic ( inventory , voidTearWave . Participants [ 0 ] ) ;
MissionRewards . push ( { StoreItem : reward.type , ItemCount : reward.itemCount } ) ;
2025-02-25 04:38:47 -08:00
}
2025-02-06 07:11:31 -08:00
return { inventoryChanges , MissionRewards , credits } ;
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-01-24 14:13:21 +01:00
inventory : HydratedDocument < IInventoryDatabase > ,
{
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 }
) = > {
const hasDailyCreditBonus = true ;
const totalCredits = missionDropCredits + missionCompletionCredits + rngRewardCredits ;
const finalCredits = {
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 [ ]
) = > {
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 ) ;
}
}
return missionBonusCredits ;
} ;
2025-01-24 14:13:21 +01:00
function getLevelCreditRewards ( nodeName : string ) : number {
const minEnemyLevel = getNode ( nodeName ) . minEnemyLevel ;
return 1000 + ( minEnemyLevel - 1 ) * 100 ;
//TODO: get dark sektor fixed credit rewards and railjack bonus
}
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 ;
}