import { ExportEnemies, ExportFusionBundles, ExportRegions, ExportRewards, IMissionReward as IMissionRewardExternal, IReward } from "warframe-public-export-plus"; import { IMissionInventoryUpdateRequest, IRewardInfo } from "../types/requestTypes"; import { logger } from "@/src/utils/logger"; import { IRngResult, getRandomElement, getRandomReward } from "@/src/services/rngService"; import { equipmentKeys, IInventoryDatabase, TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; import { addChallenges, addConsumables, addCrewShipAmmo, addCrewShipRawSalvage, addEmailItem, addFocusXpIncreases, addFusionTreasures, addGearExpByCategory, addItem, addMiscItems, addMissionComplete, addMods, addRecipes, combineInventoryChanges, updateSyndicate } from "@/src/services/inventoryService"; import { updateQuestKey } from "@/src/services/questService"; import { HydratedDocument } from "mongoose"; import { IInventoryChanges } from "@/src/types/purchaseTypes"; import { getLevelKeyRewards, getNode, toStoreItem } from "@/src/services/itemDataService"; import { InventoryDocumentProps, TInventoryDatabaseDocument } from "@/src/models/inventoryModels/inventoryModel"; import { getEntriesUnsafe } from "@/src/utils/ts-utils"; import { IEquipmentClient } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { handleStoreItemAcquisition } from "./purchaseService"; import { IMissionReward } from "../types/missionTypes"; import { crackRelic } from "@/src/helpers/relicHelper"; 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"; const getRotations = (rotationCount: number): number[] => { if (rotationCount === 0) return [0]; const rotationPattern = [0, 0, 1, 2]; // A, A, B, C const rotatedValues = []; for (let i = 0; i < rotationCount; i++) { rotatedValues.push(rotationPattern[i % rotationPattern.length]); } return rotatedValues; }; const getRandomRewardByChance = (pool: IReward[]): IRngResult | undefined => { return getRandomReward(pool as IRngResult[]); }; //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 = async ( inventory: HydratedDocument, inventoryUpdates: IMissionInventoryUpdateRequest ): Promise => { const inventoryChanges: IInventoryChanges = {}; 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() }); } } if (inventoryUpdates.RewardInfo && inventoryUpdates.RewardInfo.NemesisAbandonedRewards) { inventory.NemesisAbandonedRewards = inventoryUpdates.RewardInfo.NemesisAbandonedRewards; } 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": await 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": case "BonusMiscItems": addMiscItems(inventory, value); break; case "Consumables": addConsumables(inventory, value); break; case "Recipes": addRecipes(inventory, value); break; case "ChallengeProgress": addChallenges(inventory, value); break; case "FusionTreasures": addFusionTreasures(inventory, value); break; case "CrewShipRawSalvage": addCrewShipRawSalvage(inventory, value); break; case "CrewShipAmmo": addCrewShipAmmo(inventory, value); break; case "FusionBundles": { let fusionPoints = 0; for (const fusionBundle of value) { const fusionPointsTotal = ExportFusionBundles[fusionBundle.ItemType].fusionPoints * fusionBundle.ItemCount; inventory.FusionPoints += fusionPointsTotal; fusionPoints += fusionPointsTotal; } inventoryChanges.FusionPoints = fusionPoints; break; } case "EmailItems": { for (const tc of value) { await addEmailItem(inventory, tc.ItemType); } break; } case "FocusXpIncreases": { addFocusXpIncreases(inventory, value); break; } case "PlayerSkillGains": { inventory.PlayerSkills.LPP_SPACE += value.LPP_SPACE; inventory.PlayerSkills.LPP_DRIFTER += value.LPP_DRIFTER; break; } 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; } case "LoreFragmentScans": value.forEach(clientFragment => { const fragment = inventory.LoreFragmentScans.find(x => x.ItemType == clientFragment.ItemType); if (fragment) { fragment.Progress += clientFragment.Progress; } else { inventory.LoreFragmentScans.push(clientFragment); } }); break; case "LibraryScans": value.forEach(scan => { 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 ]; } 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`); } }); break; case "CollectibleScans": for (const scan of value) { 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; for (const gate of entry.IncentiveStates) { gate.complete = progress >= gate.threshold; if (gate.complete && !gate.sent) { gate.sent = true; if (gate.threshold == 0.5) { await createMessage(inventory.accountOwnerId.toString(), [kuriaMessage50]); } else { await createMessage(inventory.accountOwnerId.toString(), [kuriaMessage75]); } } } if (progress >= 1.0) { await createMessage(inventory.accountOwnerId.toString(), [kuriaMessage100]); } } } else { logger.warn(`${scan.CollectibleType} was not found in inventory, ignoring scans`); } } break; 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; case "SyndicateId": { inventory.CompletedSyndicates.push(value); break; } case "SortieId": { inventory.CompletedSorties.push(value); break; } case "SeasonChallengeCompletions": { const processedCompletions = value.map(({ challenge, id }) => ({ challenge: challenge.substring(challenge.lastIndexOf("/") + 1), id })); inventory.SeasonChallengeHistory.push(...processedCompletions); break; } 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. await createMessage(inventory.accountOwnerId.toString(), [ { 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; } default: // Equipment XP updates if (equipmentKeys.includes(key as TEquipmentKey)) { const multipliedValue = (value as IEquipmentClient[]).map(equipment => { // 生成一个 0 到 1 的随机数 const randomChance = Math.random(); // 基础倍率为 5 倍 let multiplier = 5; // 10% 的概率触发 10 倍经验 if (randomChance < 0.1) { multiplier = 10; // ✅ 10% 概率 10 倍经验 } // 计算最终经验值 const finalXP = (equipment.XP ?? 0) * multiplier; // 日志输出(中文) logger.debug(`[经验倍率] 随机数: ${randomChance.toFixed(2)}, 倍率: ${multiplier}, 最终经验值: ${finalXP}`); return { ...equipment, XP: finalXP // ✅ 处理 undefined }; }); addGearExpByCategory(inventory, multipliedValue, key as TEquipmentKey); } break; // if ( // (ignoredInventoryUpdateKeys as readonly string[]).includes(key) || // knownUnhandledKeys.includes(key) // ) { // continue; // } // logger.error(`Unhandled inventory update key: ${key}`); } } return inventoryChanges; }; interface AddMissionRewardsReturnType { MissionRewards: IMissionReward[]; inventoryChanges?: IInventoryChanges; credits?: IMissionCredits; } //TODO: return type of partial missioninventoryupdate response export const addMissionRewards = async ( inventory: TInventoryDatabaseDocument, { RewardInfo: rewardInfo, LevelKeyName: levelKeyName, Missions: missions, RegularCredits: creditDrops, VoidTearParticipantsCurrWave: voidTearWave, StrippedItems: strippedItems }: IMissionInventoryUpdateRequest ): Promise => { if (!rewardInfo) { //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: [] }; } //TODO: check double reward merging const MissionRewards: IMissionReward[] = getRandomMissionDrops(rewardInfo); logger.debug("random mission drops:", MissionRewards); const inventoryChanges: IInventoryChanges = {}; let missionCompletionCredits = 0; //inventory change is what the client has not rewarded itself, also the client needs to know the credit changes for display if (levelKeyName) { const fixedLevelRewards = getLevelKeyRewards(levelKeyName); //logger.debug(`fixedLevelRewards ${fixedLevelRewards}`); 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 }); } } } if ( missions && missions.Tag != "" // https://onlyg.it/OpenWF/SpaceNinjaServer/issues/1013 ) { const node = getNode(missions.Tag); //node based credit rewards for mission completion if (node.missionIndex !== 28) { const levelCreditReward = getLevelCreditRewards(missions.Tag); missionCompletionCredits += levelCreditReward; inventory.RegularCredits += levelCreditReward; logger.debug(`levelCreditReward ${levelCreditReward}`); } if (node.missionReward) { missionCompletionCredits += addFixedLevelRewards(node.missionReward, inventory, MissionRewards); } } if (rewardInfo.useVaultManifest) { MissionRewards.push({ StoreItem: getRandomElement(corruptedMods), ItemCount: 1 }); } for (const reward of MissionRewards) { const inventoryChange = await handleStoreItemAcquisition(reward.StoreItem, inventory, reward.ItemCount); //TODO: combineInventoryChanges improve type safety, merging 2 of the same item? //TODO: check for the case when two of the same item are added, combineInventoryChanges should merge them, 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 }); if ( voidTearWave && voidTearWave.Participants[0].QualifiesForReward && !voidTearWave.Participants[0].HaveRewardResponse ) { const reward = await crackRelic(inventory, voidTearWave.Participants[0], inventoryChanges); MissionRewards.push({ StoreItem: reward.type, ItemCount: reward.itemCount }); } 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" }); } } } } } return { inventoryChanges, MissionRewards, credits }; }; interface IMissionCredits { MissionCredits: number[]; CreditBonus: number[]; TotalCredits: number[]; DailyMissionBonus?: boolean; } //creditBonus is not entirely accurate. //TODO: consider ActiveBoosters export const addCredits = ( inventory: HydratedDocument, { missionDropCredits, missionCompletionCredits, rngRewardCredits }: { missionDropCredits: number; missionCompletionCredits: number; rngRewardCredits: number } ): IMissionCredits => { const hasDailyCreditBonus = true; const totalCredits = missionDropCredits + missionCompletionCredits + rngRewardCredits; const finalCredits: IMissionCredits = { MissionCredits: [missionDropCredits, missionDropCredits], CreditBonus: [missionCompletionCredits, missionCompletionCredits], TotalCredits: [totalCredits, totalCredits] }; if (hasDailyCreditBonus) { inventory.RegularCredits += missionCompletionCredits; finalCredits.CreditBonus[1] *= 2; finalCredits.MissionCredits[1] *= 2; finalCredits.TotalCredits[1] *= 2; } if (!hasDailyCreditBonus) { return finalCredits; } return { ...finalCredits, DailyMissionBonus: true }; }; export const addFixedLevelRewards = ( rewards: IMissionRewardExternal, inventory: TInventoryDatabaseDocument, MissionRewards: IMissionReward[] ): number => { let missionBonusCredits = 0; if (rewards.credits) { missionBonusCredits += rewards.credits; inventory.RegularCredits += rewards.credits; } if (rewards.items) { for (const item of rewards.items) { MissionRewards.push({ StoreItem: item, ItemCount: 1 }); } } if (rewards.countedItems) { for (const item of rewards.countedItems) { MissionRewards.push({ StoreItem: `/Lotus/StoreItems${item.ItemType.substring("Lotus/".length)}`, ItemCount: item.ItemCount }); } } if (rewards.countedStoreItems) { for (const item of rewards.countedStoreItems) { MissionRewards.push(item); } } return missionBonusCredits; }; function getLevelCreditRewards(nodeName: string): number { const minEnemyLevel = getNode(nodeName).minEnemyLevel; return 1000 + (minEnemyLevel - 1) * 100; //TODO: get dark sektor fixed credit rewards and railjack bonus } function getRandomMissionDrops(RewardInfo: IRewardInfo): IMissionReward[] { const drops: IMissionReward[] = []; // 模糊匹配 jobId 并处理奖励 if (RewardInfo.jobId) { // 定义任务类型和对应的奖励表、声望阵营及声望值 const jobRewardsMap: Record = { "AssassinateBountyCap": { rewardManifest: "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierATableCRewards", tag: "CetusSyndicate", standingValue: 430 }, "DeimosGrnSurvivorBounty": { rewardManifest: "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierATableBRewards", tag: "EntratiSyndicate", standingValue: 450 }, "DeimosAreaDefenseBounty": { rewardManifest: "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierCTableBRewards", tag: "EntratiSyndicate", standingValue: 500 }, "DeimosEndlessExcavateBounty": { rewardManifest: "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierBTableARewards", tag: "EntratiSyndicate", standingValue: 550 }, "DeimosAssassinateBounty": { rewardManifest: "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierDTableBRewards", tag: "EntratiSyndicate", standingValue: 600 }, "DeimosKeyPiecesBounty": { rewardManifest: "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierETableARewards", tag: "EntratiSyndicate", standingValue: 650 }, "DeimosExcavateBounty": { rewardManifest: "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/TierETableARewards", tag: "EntratiSyndicate", standingValue: 700 }, "VenusIntelJobSpy": { rewardManifest: "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierATableBRewards", tag: "SolarisSyndicate", standingValue: 450 }, "VenusCullJobResource": { rewardManifest: "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierBTableBRewards", tag: "SolarisSyndicate", standingValue: 500 }, "VenusIntelJobRecovery": { rewardManifest: "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierCTableBRewards", tag: "SolarisSyndicate", standingValue: 550 }, "VenusHelpingJobCaches": { rewardManifest: "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierDTableBRewards", tag: "SolarisSyndicate", standingValue: 600 }, "VenusArtifactJobAmbush": { rewardManifest: "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETableBRewards", tag: "SolarisSyndicate", standingValue: 650 }, "VenusChaosJobExcavation": { rewardManifest: "/Lotus/Types/Game/MissionDecks/VenusJobMissionRewards/VenusTierETableBRewards", tag: "SolarisSyndicate", standingValue: 700 }, "NarmerVenusCullJobExterminate": { rewardManifest: "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/NarmerTableBRewards", tag: "SolarisSyndicate", standingValue: 800 }, "AttritionBountyLib": { rewardManifest: "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierBTableBRewards", tag: "CetusSyndicate", standingValue: 500 }, "RescueBountyResc": { rewardManifest: "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierCTableBRewards", tag: "CetusSyndicate", standingValue: 550 }, "CaptureBountyCapTwo": { rewardManifest: "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierDTableBRewards", tag: "CetusSyndicate", standingValue: 600 }, "ReclamationBountyCache": { rewardManifest: "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierETableBRewards", tag: "CetusSyndicate", standingValue: 650 }, "AttritionBountyCap": { rewardManifest: "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/TierETableBRewards", tag: "CetusSyndicate", standingValue: 700 }, "ChamberB": { rewardManifest: "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierATableCRewards", tag: "EntratiSyndicate", standingValue: 500 }, "ChamberA": { rewardManifest: "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierBTableCRewards", tag: "EntratiSyndicate", standingValue: 800 }, "Chamberc": { rewardManifest: "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierCTableCRewards", tag: "EntratiSyndicate", standingValue: 1000 }, "HeistProfitTakerBountyOne": { rewardManifest: "/Lotus/Types/Game/MissionDecks/DeimosMissionRewards/VaultBountyTierCTableCRewards111", tag: "EntratiSyndicate", standingValue: 1000 }, "AssassinateBountyAss": { rewardManifest: "/Lotus/Types/Game/MissionDecks/EidolonJobMissionRewards/NarmerTableBRewards", tag: "CetusSyndicate", standingValue: 800 } }; // 遍历任务类型,模糊匹配 jobId for (const [jobType, { rewardManifest, tag, standingValue }] of Object.entries(jobRewardsMap)) { if (RewardInfo.jobId.includes(jobType)) { logger.debug(`Job ID contains ${jobType}, using reward manifest: ${rewardManifest}`); const rewardTable = ExportRewards[rewardManifest]; if (rewardTable) { // 使用 JobStage 作为轮次索引 let rotation = RewardInfo.JobStage || 0; // 默认值为 0 logger.debug("Using JobStage as rotation index:", rotation); // 如果 JobStage 超过 3,则按最高档(第 3 档)处理 if (rotation > 3) { rotation = 3; logger.debug("JobStage exceeds 3, using highest rotation (3)"); } // 检查轮次索引是否在奖励表范围内 if (rotation >= rewardTable.length || rotation < 0) { logger.error(`Rotation index ${rotation} is out of bounds for reward table ${rewardManifest}`); } else { // 获取当前轮次的奖励池 const rotationRewards = rewardTable[rotation]; logger.debug("Rotation rewards:", rotationRewards); // 从奖励池中随机选择一个奖励 const drop = getRandomRewardByChance(rotationRewards); if (drop) { logger.debug("Random drop selected:", drop); drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount }); } else { logger.debug("No drop selected from reward table"); } } } else { logger.error(`Reward table ${rewardManifest} not found in ExportRewards`); } // 新增一个固定的物品奖励 const additionalReward = { StoreItem: "/Lotus/StoreItems/Types/Items/SyndicateDogTags/UniversalSyndicateDogTag", // 新物品的路径 ItemCount: 1 // 物品数量 }; drops.push(additionalReward); logger.debug("Added additional reward:", additionalReward); // 直接返回,不再执行后续的区域奖励逻辑 return drops; } } } if (RewardInfo.node in ExportRegions) { const region = ExportRegions[RewardInfo.node]; const rewardManifests: string[] = RewardInfo.periodicMissionTag == "EliteAlert" || RewardInfo.periodicMissionTag == "EliteAlertB" ? ["/Lotus/Types/Game/MissionDecks/EliteAlertMissionRewards/EliteAlertMissionRewards"] : region.rewardManifests; 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); } rewardManifests .map(name => ExportRewards[name]) .forEach(table => { for (const rotation of rotations) { const rotationRewards = table[rotation]; const drop = getRandomRewardByChance(rotationRewards); // 原始掉落逻辑 if (drop) { drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount }); } // EliteAlert奖励逻辑 if (RewardInfo.periodicMissionTag === "EliteAlert" || RewardInfo.periodicMissionTag === "EliteAlertB") { const randomCount = Math.floor(Math.random() * 5) + 1; drops.push({ StoreItem: "/Lotus/StoreItems/Types/Items/MiscItems/Elitium", ItemCount: randomCount }); } // 添加 HardDaily 任务的 钢铁精华 掉落 if (RewardInfo.periodicMissionTag?.startsWith("HardDaily")) { let randomCount = Math.floor(Math.random() * 5) + 1; // 生成 1 到 5 的随机数 // 20% 的几率翻 1 到 10 倍 if (Math.random() < 0.2) { const multiplier = Math.floor(Math.random() * 10) + 1; // 生成 1 到 10 的随机倍数 randomCount *= multiplier; } drops.push({ StoreItem: "/Lotus/StoreItems/Types/Items/MiscItems/SteelEssence", ItemCount: randomCount }); } // 新增10%概率独立掉落 ▼▼▼ if (Math.random() < 0.01) { // 每个rotation独立判定 drops.push({ StoreItem: "/Lotus/StoreItems/Upgrades/Skins/Volt/SWTechnoshockHelmet", ItemCount: 1 }); } } }); if (region.cacheRewardManifest && RewardInfo.EnemyCachesFound) { const deck = ExportRewards[region.cacheRewardManifest]; for (let rotation = 0; rotation != RewardInfo.EnemyCachesFound; ++rotation) { const drop = getRandomRewardByChance(deck[rotation]); if (drop) { drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount, FromEnemyCache: true }); } } } if (RewardInfo.nightmareMode) { const deck = ExportRewards["/Lotus/Types/Game/MissionDecks/NightmareModeRewards"]; let rotation = 0; // 确保 region 已正确初始化 if (region) { if (region.missionIndex === 3 && RewardInfo.rewardTier) { // 如果 missionIndex 为 3 且 rewardTier 存在,则使用 rewardTier rotation = RewardInfo.rewardTier; } else if ([6, 7, 8, 10, 11].includes(region.systemIndex)) { // 如果 systemIndex 在 [6, 7, 8, 10, 11] 中,则 rotation 为 2 rotation = 2; } else if ([4, 9, 12, 14, 15, 16, 17, 18].includes(region.systemIndex)) { // 如果 systemIndex 在 [4, 9, 12, 14, 15, 16, 17, 18] 中,则 rotation 为 1 rotation = 1; } } // 确保 rotation 在 deck 的范围内 if (rotation >= deck.length || rotation < 0) { logger.error(`Rotation index ${rotation} is out of bounds for NightmareModeRewards`); rotation = 0; // 如果超出范围,则使用默认值 0 } // 获取当前轮次的奖励池 const rotationRewards = deck[rotation]; if (rotationRewards) { // 从奖励池中随机选择一个奖励 const drop = getRandomRewardByChance(rotationRewards); if (drop) { logger.debug("Nightmare mode drop selected:", drop); drops.push({ StoreItem: drop.type, ItemCount: drop.itemCount }); } else { logger.debug("No drop selected from NightmareModeRewards"); } } else { logger.error("No rewards found for NightmareModeRewards"); } } } // 确保函数有返回值 return drops; } 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 ]; const libraryPersonalTargetToAvatar: Record = { "/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" };