feat: initial invasions (#2458)

A rough generation of 3 invasions that change at daily reset, so missing the planet-based invasion 'chains'.
Battle pay is fully working tho, just a few points of uncertainty there due to missing research and logs.
Death marks are also roughly working.
Re #1097

Reviewed-on: OpenWF/SpaceNinjaServer#2458
Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com>
Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com>
This commit is contained in:
Sainan 2025-07-09 19:58:01 -07:00 committed by Sainan
parent dc8f32d4d8
commit 7eb95c995c
7 changed files with 488 additions and 44 deletions

View File

@ -6,10 +6,18 @@ import allDialogue from "@/static/fixed_responses/allDialogue.json";
import { ILoadoutDatabase } from "@/src/types/saveLoadoutTypes"; import { ILoadoutDatabase } from "@/src/types/saveLoadoutTypes";
import { IInventoryClient, IShipInventory, equipmentKeys } from "@/src/types/inventoryTypes/inventoryTypes"; import { IInventoryClient, IShipInventory, equipmentKeys } from "@/src/types/inventoryTypes/inventoryTypes";
import { IPolarity, ArtifactPolarity } from "@/src/types/inventoryTypes/commonInventoryTypes"; import { IPolarity, ArtifactPolarity } from "@/src/types/inventoryTypes/commonInventoryTypes";
import { ExportCustoms, ExportFlavour, ExportResources, ExportVirtuals } from "warframe-public-export-plus"; import {
eFaction,
ExportCustoms,
ExportFlavour,
ExportResources,
ExportVirtuals,
ICountedItem
} from "warframe-public-export-plus";
import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "@/src/services/infestedFoundryService"; import { applyCheatsToInfestedFoundry, handleSubsumeCompletion } from "@/src/services/infestedFoundryService";
import { import {
addEmailItem, addEmailItem,
addItem,
addMiscItems, addMiscItems,
allDailyAffiliationKeys, allDailyAffiliationKeys,
checkCalendarAutoAdvance, checkCalendarAutoAdvance,
@ -30,7 +38,8 @@ import { unixTimesInMs } from "@/src/constants/timeConstants";
import { DailyDeal } from "@/src/models/worldStateModel"; import { DailyDeal } from "@/src/models/worldStateModel";
import { EquipmentFeatures } from "@/src/types/equipmentTypes"; import { EquipmentFeatures } from "@/src/types/equipmentTypes";
import { generateRewardSeed } from "@/src/services/rngService"; import { generateRewardSeed } from "@/src/services/rngService";
import { getWorldState } from "@/src/services/worldStateService"; import { getInvasionByOid, getWorldState } from "@/src/services/worldStateService";
import { createMessage } from "@/src/services/inboxService";
export const inventoryController: RequestHandler = async (request, response) => { export const inventoryController: RequestHandler = async (request, response) => {
const account = await getAccountForRequest(request); const account = await getAccountForRequest(request);
@ -186,6 +195,63 @@ export const inventoryController: RequestHandler = async (request, response) =>
//await inventory.save(); //await inventory.save();
} }
for (let i = 0; i != inventory.QualifyingInvasions.length; ) {
const qi = inventory.QualifyingInvasions[i];
const invasion = getInvasionByOid(qi.invasionId.toString());
if (!invasion) {
logger.debug(`removing QualifyingInvasions entry for unknown invasion: ${qi.invasionId.toString()}`);
inventory.QualifyingInvasions.splice(i, 1);
continue;
}
if (invasion.Completed) {
let factionSidedWith: string | undefined;
let battlePay: ICountedItem[] | undefined;
if (qi.AttackerScore >= 3) {
factionSidedWith = invasion.Faction;
battlePay = invasion.AttackerReward.countedItems;
logger.debug(`invasion pay from ${factionSidedWith}`, { battlePay });
} else if (qi.DefenderScore >= 3) {
factionSidedWith = invasion.DefenderFaction;
battlePay = invasion.DefenderReward.countedItems;
logger.debug(`invasion pay from ${factionSidedWith}`, { battlePay });
}
if (factionSidedWith) {
if (battlePay) {
// Decoupling rewards from the inbox message because it may delete itself without being read
for (const item of battlePay) {
await addItem(inventory, item.ItemType, item.ItemCount);
}
await createMessage(account._id, [
{
sndr: eFaction.find(x => x.tag == factionSidedWith)?.name ?? factionSidedWith, // TOVERIFY
msg: `/Lotus/Language/G1Quests/${factionSidedWith}_InvasionThankyouMessageBody`,
sub: `/Lotus/Language/G1Quests/${factionSidedWith}_InvasionThankyouMessageSubject`,
countedAtt: battlePay,
attVisualOnly: true,
icon:
factionSidedWith == "FC_GRINEER"
? "/Lotus/Interface/Icons/Npcs/EliteRifleLancerAvatar.png" // Source: https://www.reddit.com/r/Warframe/comments/1aj4usx/battle_pay_worth_10_plat/, https://www.youtube.com/watch?v=XhNZ6ai6BOY
: "/Lotus/Interface/Icons/Npcs/CrewmanNormal.png", // My best source for this is https://www.youtube.com/watch?v=rxrCCFm73XE around 1:37
// TOVERIFY: highPriority?
endDate: 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 not clear if this is correct.
}
]);
}
if (invasion.Faction != "FC_INFESTATION") {
// Sided with grineer -> opposed corpus -> send zanuka (harvester)
// Sided with corpus -> opposed grineer -> send g3 (death squad)
inventory[factionSidedWith != "FC_GRINEER" ? "DeathSquadable" : "Harvestable"] = true;
// TOVERIFY: Should this happen earlier?
// TOVERIFY: Should this send an (ephemeral) email?
}
}
logger.debug(`removing QualifyingInvasions entry for completed invasion: ${qi.invasionId.toString()}`);
inventory.QualifyingInvasions.splice(i, 1);
continue;
}
++i;
}
if (inventory.LastInventorySync) { if (inventory.LastInventorySync) {
const lastSyncDuviriMood = Math.trunc(inventory.LastInventorySync.getTimestamp().getTime() / 7200000); const lastSyncDuviriMood = Math.trunc(inventory.LastInventorySync.getTimestamp().getTime() / 7200000);
const currentDuviriMood = Math.trunc(Date.now() / 7200000); const currentDuviriMood = Math.trunc(Date.now() / 7200000);

View File

@ -558,6 +558,7 @@ export const addMissionInventoryUpdates = async (
} }
]); ]);
} }
inventory.DeathSquadable = false;
break; break;
} }
case "LockedWeaponGroup": { case "LockedWeaponGroup": {
@ -576,7 +577,7 @@ export const addMissionInventoryUpdates = async (
break; break;
} }
case "IncHarvester": { case "IncHarvester": {
inventory.Harvestable = true; // Unsure what to do with this
break; break;
} }
case "CurrentLoadOutIds": { case "CurrentLoadOutIds": {

View File

@ -6,15 +6,18 @@ import sortieTilesets from "@/static/fixed_responses/worldState/sortieTilesets.j
import sortieTilesetMissions from "@/static/fixed_responses/worldState/sortieTilesetMissions.json"; import sortieTilesetMissions from "@/static/fixed_responses/worldState/sortieTilesetMissions.json";
import syndicateMissions from "@/static/fixed_responses/worldState/syndicateMissions.json"; import syndicateMissions from "@/static/fixed_responses/worldState/syndicateMissions.json";
import darvoDeals from "@/static/fixed_responses/worldState/darvoDeals.json"; import darvoDeals from "@/static/fixed_responses/worldState/darvoDeals.json";
import invasionNodes from "@/static/fixed_responses/worldState/invasionNodes.json";
import invasionRewards from "@/static/fixed_responses/worldState/invasionRewards.json";
import { buildConfig } from "@/src/services/buildConfigService"; import { buildConfig } from "@/src/services/buildConfigService";
import { unixTimesInMs } from "@/src/constants/timeConstants"; import { unixTimesInMs } from "@/src/constants/timeConstants";
import { config } from "@/src/services/configService"; import { config } from "@/src/services/configService";
import { getRandomElement, getRandomInt, sequentiallyUniqueRandomElement, SRng } from "@/src/services/rngService"; import { getRandomElement, getRandomInt, sequentiallyUniqueRandomElement, SRng } from "@/src/services/rngService";
import { eMissionType, ExportRegions, ExportSyndicates, IRegion } from "warframe-public-export-plus"; import { eMissionType, ExportRegions, ExportSyndicates, IMissionReward, IRegion } from "warframe-public-export-plus";
import { import {
ICalendarDay, ICalendarDay,
ICalendarEvent, ICalendarEvent,
ICalendarSeason, ICalendarSeason,
IInvasion,
ILiteSortie, ILiteSortie,
IPrimeVaultTrader, IPrimeVaultTrader,
IPrimeVaultTraderOffer, IPrimeVaultTraderOffer,
@ -1227,6 +1230,78 @@ const getAllVarziaManifests = (): IPrimeVaultTraderOffer[] => {
return [...dualPacks, ...singlePacks, ...items, ...bobbleHeads, ...relics]; return [...dualPacks, ...singlePacks, ...items, ...bobbleHeads, ...relics];
}; };
const createInvasion = (day: number, idx: number): IInvasion => {
const id = day * 3 + idx;
const defender = (["FC_GRINEER", "FC_CORPUS", day % 2 ? "FC_GRINEER" : "FC_CORPUS"] as const)[idx];
const rng = new SRng(new SRng(id).randomInt(0, 1_000_000));
const isInfestationOutbreak = rng.randomInt(0, 1) == 0;
const attacker = isInfestationOutbreak ? "FC_INFESTATION" : defender == "FC_GRINEER" ? "FC_CORPUS" : "FC_GRINEER";
const startMs = EPOCH + day * 86400_000;
const oid =
((startMs / 1000) & 0xffffffff).toString(16).padStart(8, "0") +
"fd148cb8" +
(idx & 0xffffffff).toString(16).padStart(8, "0");
const node = sequentiallyUniqueRandomElement(invasionNodes[defender], id, 5, 690175)!; // Can't repeat the other 2 on this day nor the last 3
const progress = (Date.now() - startMs) / 86400_000;
const countMultiplier = isInfestationOutbreak || rng.randomInt(0, 1) ? -1 : 1; // if defender is winning, count is negative
const fiftyPercent = rng.randomInt(1000, 29000); // introduce some 'yitter' for the percentages
const rewardFloat = rng.randomFloat();
const rewardTier = rewardFloat < 0.201 ? "RARE" : rewardFloat < 0.7788 ? "COMMON" : "UNCOMMON";
const attackerReward: IMissionReward = {};
const defenderReward: IMissionReward = {};
if (isInfestationOutbreak) {
defenderReward.countedItems = [
rng.randomElement(invasionRewards[rng.randomInt(0, 1) ? "FC_INFESTATION" : defender][rewardTier])!
];
} else {
attackerReward.countedItems = [rng.randomElement(invasionRewards[attacker][rewardTier])!];
defenderReward.countedItems = [rng.randomElement(invasionRewards[defender][rewardTier])!];
}
return {
_id: { $oid: oid },
Faction: attacker,
DefenderFaction: defender,
Node: node,
Count: Math.round(
(progress < 0.5 ? progress * 2 * fiftyPercent : fiftyPercent + (30_000 - fiftyPercent) * (progress - 0.5)) *
countMultiplier
),
Goal: 30000, // Value seems to range from 30000 to 98000 in intervals of 1000. Higher values are increasingly rare. I don't think this is relevant for the frontend besides dividing count by it.
LocTag: isInfestationOutbreak
? ExportRegions[node].missionIndex == 0
? "/Lotus/Language/Menu/InfestedInvasionBoss"
: "/Lotus/Language/Menu/InfestedInvasionGeneric"
: attacker == "FC_CORPUS"
? "/Lotus/Language/Menu/CorpusInvasionGeneric"
: "/Lotus/Language/Menu/GrineerInvasionGeneric",
Completed: startMs + 86400_000 < Date.now(), // Sorta unfaithful. Invasions on live are (at least in part) in fluenced by people completing them. And otherwise also probably not hardcoded to last 24 hours.
ChainID: { $oid: oid },
AttackerReward: attackerReward,
AttackerMissionInfo: {
seed: rng.randomInt(0, 1_000_000),
faction: defender
},
DefenderReward: defenderReward,
DefenderMissionInfo: {
seed: rng.randomInt(0, 1_000_000),
faction: attacker
},
Activation: {
$date: {
$numberLong: startMs.toString()
}
}
};
};
export const getInvasionByOid = (oid: string): IInvasion | undefined => {
const arr = oid.split("fd148cb8");
if (arr.length == 2 && arr[0].length == 8 && arr[1].length == 8) {
return createInvasion(idToDay(oid), parseInt(arr[1], 16));
}
return undefined;
};
export const getWorldState = (buildLabel?: string): IWorldState => { export const getWorldState = (buildLabel?: string): IWorldState => {
const constraints: ITimeConstraint[] = []; const constraints: ITimeConstraint[] = [];
if (config.worldState?.eidolonOverride) { if (config.worldState?.eidolonOverride) {
@ -1275,6 +1350,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
LiteSorties: [], LiteSorties: [],
ActiveMissions: [], ActiveMissions: [],
GlobalUpgrades: [], GlobalUpgrades: [],
Invasions: [],
VoidTraders: [], VoidTraders: [],
PrimeVaultTraders: [], PrimeVaultTraders: [],
VoidStorms: [], VoidStorms: [],
@ -1477,6 +1553,20 @@ export const getWorldState = (buildLabel?: string): IWorldState => {
}); });
} }
// Rough outline of dynamic invasions.
// TODO: Invasions chains, e.g. an infestation mission would soon lead to other nodes on that planet also having an infestation invasion.
// TODO: Grineer/Corpus to fund their death stars with each invasion win.
{
worldState.Invasions.push(createInvasion(day, 0));
worldState.Invasions.push(createInvasion(day, 1));
worldState.Invasions.push(createInvasion(day, 2));
// Completed invasions stay for up to 24 hours as the winner 'occupies' that node
worldState.Invasions.push(createInvasion(day - 1, 0));
worldState.Invasions.push(createInvasion(day - 1, 1));
worldState.Invasions.push(createInvasion(day - 1, 2));
}
// Baro // Baro
{ {
const baroIndex = Math.trunc((Date.now() - 910800000) / (unixTimesInMs.day * 14)); const baroIndex = Math.trunc((Date.now() - 910800000) / (unixTimesInMs.day * 14));

View File

@ -12,6 +12,7 @@ export interface IWorldState {
SyndicateMissions: ISyndicateMissionInfo[]; SyndicateMissions: ISyndicateMissionInfo[];
ActiveMissions: IFissure[]; ActiveMissions: IFissure[];
GlobalUpgrades: IGlobalUpgrade[]; GlobalUpgrades: IGlobalUpgrade[];
Invasions: IInvasion[];
NodeOverrides: INodeOverride[]; NodeOverrides: INodeOverride[];
VoidTraders: IVoidTrader[]; VoidTraders: IVoidTrader[];
PrimeVaultTraders: IPrimeVaultTrader[]; PrimeVaultTraders: IPrimeVaultTrader[];
@ -82,6 +83,28 @@ export interface IGlobalUpgrade {
LocalizeDescTag: string; LocalizeDescTag: string;
} }
export interface IInvasion {
_id: IOid;
Faction: string;
DefenderFaction: string;
Node: string;
Count: number;
Goal: number;
LocTag: string;
Completed: boolean;
ChainID: IOid;
AttackerReward: IMissionReward;
AttackerMissionInfo: IInvasionMissionInfo;
DefenderReward: IMissionReward;
DefenderMissionInfo: IInvasionMissionInfo;
Activation: IMongoDate;
}
export interface IInvasionMissionInfo {
seed: number;
faction: string;
}
export interface IFissure { export interface IFissure {
_id: IOid; _id: IOid;
Region: number; Region: number;

View File

@ -0,0 +1,114 @@
{
"FC_CORPUS": [
"SettlementNode1",
"SettlementNode2",
"SettlementNode3",
"SettlementNode11",
"SettlementNode12",
"SettlementNode14",
"SettlementNode15",
"SettlementNode20",
"SolNode1",
"SolNode2",
"SolNode4",
"SolNode6",
"SolNode10",
"SolNode17",
"SolNode21",
"SolNode22",
"SolNode23",
"SolNode25",
"SolNode38",
"SolNode43",
"SolNode48",
"SolNode49",
"SolNode51",
"SolNode53",
"SolNode56",
"SolNode57",
"SolNode61",
"SolNode62",
"SolNode65",
"SolNode66",
"SolNode72",
"SolNode73",
"SolNode74",
"SolNode76",
"SolNode78",
"SolNode81",
"SolNode84",
"SolNode88",
"SolNode97",
"SolNode100",
"SolNode101",
"SolNode102",
"SolNode104",
"SolNode107",
"SolNode109",
"SolNode118",
"SolNode121",
"SolNode123",
"SolNode125",
"SolNode126",
"SolNode127",
"SolNode128",
"SolNode203",
"SolNode205",
"SolNode209",
"SolNode210",
"SolNode211",
"SolNode212",
"SolNode214",
"SolNode216",
"SolNode217",
"SolNode220"
],
"FC_GRINEER": [
"SolNode11",
"SolNode16",
"SolNode18",
"SolNode19",
"SolNode20",
"SolNode30",
"SolNode31",
"SolNode32",
"SolNode36",
"SolNode41",
"SolNode42",
"SolNode45",
"SolNode46",
"SolNode50",
"SolNode58",
"SolNode67",
"SolNode68",
"SolNode70",
"SolNode82",
"SolNode93",
"SolNode96",
"SolNode99",
"SolNode106",
"SolNode113",
"SolNode131",
"SolNode132",
"SolNode135",
"SolNode137",
"SolNode138",
"SolNode139",
"SolNode140",
"SolNode141",
"SolNode144",
"SolNode146",
"SolNode147",
"SolNode149",
"SolNode177",
"SolNode181",
"SolNode184",
"SolNode185",
"SolNode187",
"SolNode188",
"SolNode189",
"SolNode191",
"SolNode195",
"SolNode196"
]
}

View File

@ -0,0 +1,190 @@
{
"FC_GRINEER": {
"COMMON": [
{
"ItemType": "/Lotus/Types/Items/Research/ChemComponent",
"ItemCount": 3
}
],
"UNCOMMON": [
{
"ItemType": "/Lotus/Types/Recipes/Weapons/KarakWraithBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/KarakWraithBarrel",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/KarakWraithReceiver",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/KarakWraithStock",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/StrunWraithBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/StrunWraithBarrel",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/StrunWraithReceiver",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/StrunWraithStock",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/LatronWraithBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/LatronWraithBarrel",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/LatronWraithReceiver",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/LatronWraithStock",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/TwinVipersWraithBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/TwinVipersWraithBarrel",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/TwinVipersWraithLink",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/TwinVipersWraithReceiver",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/GrineerCombatKnifeSortieBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/GrineerCombatKnifeHilt",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/GrineerCombatKnifeBlade",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/GrineerCombatKnifeHeatsink",
"ItemCount": 1
}
],
"RARE": [
{
"ItemType": "/Lotus/Types/Recipes/Components/OrokinCatalystBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Components/OrokinReactorBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Components/FormaBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Components/UtilityUnlockerBlueprint",
"ItemCount": 1
}
]
},
"FC_CORPUS": {
"COMMON": [
{
"ItemType": "/Lotus/Types/Items/Research/EnergyComponent",
"ItemCount": 3
}
],
"UNCOMMON": [
{
"ItemType": "/Lotus/Types/Recipes/Weapons/DeraVandalBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/DeraVandalBarrel",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/DeraVandalReceiver",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/DeraVandalStock",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/SnipetronVandalBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/SnipetronVandalStock",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/SnipetronVandalReceiver",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Weapons/WeaponParts/SnipetronVandalBarrel",
"ItemCount": 1
}
],
"RARE": [
{
"ItemType": "/Lotus/Types/Recipes/Components/OrokinCatalystBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Components/OrokinReactorBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Components/FormaBlueprint",
"ItemCount": 1
},
{
"ItemType": "/Lotus/Types/Recipes/Components/UtilityUnlockerBlueprint",
"ItemCount": 1
}
]
},
"FC_INFESTATION": {
"COMMON": [
{
"ItemType": "/Lotus/Types/Items/Research/BioComponent",
"ItemCount": 1
}
],
"UNCOMMON": [
{
"ItemType": "/Lotus/Types/Items/Research/BioComponent",
"ItemCount": 2
}
],
"RARE": [
{
"ItemType": "/Lotus/Types/Items/MiscItems/InfestedAladCoordinate",
"ItemCount": 1
}
]
}
}

View File

@ -117,46 +117,6 @@
] ]
} }
}, },
"Invasions": [
{
"_id": {
"$oid": "67c8ec8b3d0d86b236c1c18f"
},
"Faction": "FC_INFESTATION",
"DefenderFaction": "FC_CORPUS",
"Node": "SolNode53",
"Count": -28558,
"Goal": 30000,
"LocTag": "/Lotus/Language/Menu/InfestedInvasionBoss",
"Completed": false,
"ChainID": {
"$oid": "67c8b6a2bde0dfd0f7c1c18d"
},
"AttackerReward": [],
"AttackerMissionInfo": {
"seed": 488863,
"faction": "FC_CORPUS"
},
"DefenderReward": {
"countedItems": [
{
"ItemType": "/Lotus/Types/Items/Research/EnergyComponent",
"ItemCount": 3
}
]
},
"DefenderMissionInfo": {
"seed": 127653,
"faction": "FC_INFESTATION",
"missionReward": []
},
"Activation": {
"$date": {
"$numberLong": "1741221003031"
}
}
}
],
"SyndicateMissions": [ "SyndicateMissions": [
{ {
"_id": { "$oid": "663a4fc5ba6f84724fa4804c" }, "_id": { "$oid": "663a4fc5ba6f84724fa4804c" },