From dc8f32d4d8dc56b77416d23fdd01b0d46269c11a Mon Sep 17 00:00:00 2001 From: Corvus Date: Tue, 8 Jul 2025 22:12:26 -0700 Subject: [PATCH 1/6] chore(webui): update Chinese translation (#2453) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2453 Co-authored-by: Corvus Co-committed-by: Corvus --- static/webui/translations/zh.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index b7efc8c5..b695fc29 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -4,7 +4,7 @@ dict = { general_addButton: `添加`, general_setButton: `设置`, general_bulkActions: `批量操作`, - general_loading: `[UNTRANSLATED] Loading...`, + general_loading: `加载中...`, code_loginFail: `登录失败.请检查邮箱和密码.`, code_regFail: `注册失败.账号已存在.`, @@ -45,8 +45,8 @@ dict = { code_focusUnlocked: `已解锁|COUNT|个新专精学派!需要游戏内仓库更新才能生效,您可以通过访问星图来触发仓库更新.`, code_addModsConfirm: `确定要向账户添加|COUNT|张MOD吗?`, code_succImport: `导入成功。`, - code_succRelog: `[UNTRANSLATED] Done. Please note that you'll need to relog to see a difference in-game.`, - code_nothingToDo: `[UNTRANSLATED] Done. There was nothing to do.`, + code_succRelog: `完成. 需要重新登录游戏才能看到变化.`, + code_nothingToDo: `完成. 没有可执行的操作.`, code_gild: `镀金`, code_moa: `恐鸟`, code_zanuka: `猎犬`, @@ -129,7 +129,7 @@ dict = { mods_fingerprintHelp: `需要印记相关的帮助?`, mods_rivens: `裂罅MOD`, mods_mods: `Mods`, - mods_addMax: `添加(满级)`, + mods_addMax: `满级添加`, mods_addMissingUnrankedMods: `添加所有缺失的Mods`, mods_removeUnranked: `删除所有未升级的Mods`, mods_addMissingMaxRankMods: `添加所有缺失的满级Mods`, @@ -202,7 +202,7 @@ dict = { cheats_changeSupportedSyndicate: `支持的集团`, cheats_changeButton: `更改`, cheats_none: `无`, - cheats_markAllAsRead: `[UNTRANSLATED] Mark Inbox As Read`, + cheats_markAllAsRead: `收件箱全部标记为已读`, worldState: `世界状态配置`, worldState_creditBoost: `现金加成`, From 7eb95c995cbd7306b7392b8ce6b75fa078cfe5c2 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 9 Jul 2025 19:58:01 -0700 Subject: [PATCH 2/6] 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: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2458 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/inventoryController.ts | 70 ++++++- src/services/missionInventoryUpdateService.ts | 3 +- src/services/worldStateService.ts | 92 ++++++++- src/types/worldStateTypes.ts | 23 +++ .../worldState/invasionNodes.json | 114 +++++++++++ .../worldState/invasionRewards.json | 190 ++++++++++++++++++ .../worldState/worldState.json | 40 ---- 7 files changed, 488 insertions(+), 44 deletions(-) create mode 100644 static/fixed_responses/worldState/invasionNodes.json create mode 100644 static/fixed_responses/worldState/invasionRewards.json diff --git a/src/controllers/api/inventoryController.ts b/src/controllers/api/inventoryController.ts index 0d2e8ac7..357c34cb 100644 --- a/src/controllers/api/inventoryController.ts +++ b/src/controllers/api/inventoryController.ts @@ -6,10 +6,18 @@ import allDialogue from "@/static/fixed_responses/allDialogue.json"; import { ILoadoutDatabase } from "@/src/types/saveLoadoutTypes"; import { IInventoryClient, IShipInventory, equipmentKeys } from "@/src/types/inventoryTypes/inventoryTypes"; 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 { addEmailItem, + addItem, addMiscItems, allDailyAffiliationKeys, checkCalendarAutoAdvance, @@ -30,7 +38,8 @@ import { unixTimesInMs } from "@/src/constants/timeConstants"; import { DailyDeal } from "@/src/models/worldStateModel"; import { EquipmentFeatures } from "@/src/types/equipmentTypes"; 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) => { const account = await getAccountForRequest(request); @@ -186,6 +195,63 @@ export const inventoryController: RequestHandler = async (request, response) => //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) { const lastSyncDuviriMood = Math.trunc(inventory.LastInventorySync.getTimestamp().getTime() / 7200000); const currentDuviriMood = Math.trunc(Date.now() / 7200000); diff --git a/src/services/missionInventoryUpdateService.ts b/src/services/missionInventoryUpdateService.ts index cc8bd0c2..13cc449b 100644 --- a/src/services/missionInventoryUpdateService.ts +++ b/src/services/missionInventoryUpdateService.ts @@ -558,6 +558,7 @@ export const addMissionInventoryUpdates = async ( } ]); } + inventory.DeathSquadable = false; break; } case "LockedWeaponGroup": { @@ -576,7 +577,7 @@ export const addMissionInventoryUpdates = async ( break; } case "IncHarvester": { - inventory.Harvestable = true; + // Unsure what to do with this break; } case "CurrentLoadOutIds": { diff --git a/src/services/worldStateService.ts b/src/services/worldStateService.ts index 0b2dc7a5..60db22b1 100644 --- a/src/services/worldStateService.ts +++ b/src/services/worldStateService.ts @@ -6,15 +6,18 @@ import sortieTilesets from "@/static/fixed_responses/worldState/sortieTilesets.j import sortieTilesetMissions from "@/static/fixed_responses/worldState/sortieTilesetMissions.json"; import syndicateMissions from "@/static/fixed_responses/worldState/syndicateMissions.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 { unixTimesInMs } from "@/src/constants/timeConstants"; import { config } from "@/src/services/configService"; 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 { ICalendarDay, ICalendarEvent, ICalendarSeason, + IInvasion, ILiteSortie, IPrimeVaultTrader, IPrimeVaultTraderOffer, @@ -1227,6 +1230,78 @@ const getAllVarziaManifests = (): IPrimeVaultTraderOffer[] => { 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 => { const constraints: ITimeConstraint[] = []; if (config.worldState?.eidolonOverride) { @@ -1275,6 +1350,7 @@ export const getWorldState = (buildLabel?: string): IWorldState => { LiteSorties: [], ActiveMissions: [], GlobalUpgrades: [], + Invasions: [], VoidTraders: [], PrimeVaultTraders: [], 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 { const baroIndex = Math.trunc((Date.now() - 910800000) / (unixTimesInMs.day * 14)); diff --git a/src/types/worldStateTypes.ts b/src/types/worldStateTypes.ts index b83b787c..e52e613f 100644 --- a/src/types/worldStateTypes.ts +++ b/src/types/worldStateTypes.ts @@ -12,6 +12,7 @@ export interface IWorldState { SyndicateMissions: ISyndicateMissionInfo[]; ActiveMissions: IFissure[]; GlobalUpgrades: IGlobalUpgrade[]; + Invasions: IInvasion[]; NodeOverrides: INodeOverride[]; VoidTraders: IVoidTrader[]; PrimeVaultTraders: IPrimeVaultTrader[]; @@ -82,6 +83,28 @@ export interface IGlobalUpgrade { 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 { _id: IOid; Region: number; diff --git a/static/fixed_responses/worldState/invasionNodes.json b/static/fixed_responses/worldState/invasionNodes.json new file mode 100644 index 00000000..47429eac --- /dev/null +++ b/static/fixed_responses/worldState/invasionNodes.json @@ -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" + ] +} diff --git a/static/fixed_responses/worldState/invasionRewards.json b/static/fixed_responses/worldState/invasionRewards.json new file mode 100644 index 00000000..0812c342 --- /dev/null +++ b/static/fixed_responses/worldState/invasionRewards.json @@ -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 + } + ] + } +} diff --git a/static/fixed_responses/worldState/worldState.json b/static/fixed_responses/worldState/worldState.json index 1b849391..8c04c4a2 100644 --- a/static/fixed_responses/worldState/worldState.json +++ b/static/fixed_responses/worldState/worldState.json @@ -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": [ { "_id": { "$oid": "663a4fc5ba6f84724fa4804c" }, From a109ea6c5d8bd856e9f6bf4d80d198e3de91ffc1 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Wed, 9 Jul 2025 21:22:23 -0700 Subject: [PATCH 3/6] chore: update PE+ (#2459) Closes #2455 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2459 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index ca86e2cc..e83bc959 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "ncp": "^2.0.0", "typescript": "^5.5", "undici": "^7.10.0", - "warframe-public-export-plus": "^0.5.77", + "warframe-public-export-plus": "^0.5.78", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0", @@ -5479,9 +5479,9 @@ } }, "node_modules/warframe-public-export-plus": { - "version": "0.5.77", - "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.77.tgz", - "integrity": "sha512-Th/b82pYB4i95afC/s8MDDXsVMl3vyy1qDwLO/AzV6peVBTs2kCSO68nNh7yXDiviGlNl0HRq+LR25jMjtFwSg==" + "version": "0.5.78", + "resolved": "https://registry.npmjs.org/warframe-public-export-plus/-/warframe-public-export-plus-0.5.78.tgz", + "integrity": "sha512-Zvg7N+EdXS8cOAZIxqCbqiqyvQZBgh2xTxEwpHnoyJjNBpm3sP/7dtXmzHaxAZjyaCL4pvi9e7kTvxmpH8Pcag==" }, "node_modules/warframe-riven-info": { "version": "0.1.2", diff --git a/package.json b/package.json index 640a0a0b..394ca9d6 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "ncp": "^2.0.0", "typescript": "^5.5", "undici": "^7.10.0", - "warframe-public-export-plus": "^0.5.77", + "warframe-public-export-plus": "^0.5.78", "warframe-riven-info": "^0.1.2", "winston": "^3.17.0", "winston-daily-rotate-file": "^5.0.0", From 0d8044b87c2865b021c1af884d43fcc9bc9a6e75 Mon Sep 17 00:00:00 2001 From: hxedcl Date: Thu, 10 Jul 2025 20:59:22 -0700 Subject: [PATCH 4/6] chore(webui): update to Spanish translation (#2466) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2466 Co-authored-by: hxedcl Co-committed-by: hxedcl --- static/webui/translations/es.js | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index 8e1fca1c..535a024e 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -4,7 +4,7 @@ dict = { general_addButton: `Agregar`, general_setButton: `Establecer`, general_bulkActions: `Acciones masivas`, - general_loading: `[UNTRANSLATED] Loading...`, + general_loading: `Cargando...`, code_loginFail: `Error al iniciar sesión. Verifica el correo electrónico y la contraseña.`, code_regFail: `Error al registrar la cuenta. ¿Ya existe una cuenta con este correo?`, @@ -45,8 +45,8 @@ dict = { code_focusUnlocked: `¡Desbloqueadas |COUNT| nuevas escuelas de enfoque! Se necesita una actualización del inventario para reflejar los cambios en el juego. Visitar la navegación debería ser la forma más sencilla de activarlo.`, code_addModsConfirm: `¿Estás seguro de que deseas agregar |COUNT| modificadores a tu cuenta?`, code_succImport: `Importación exitosa.`, - code_succRelog: `[UNTRANSLATED] Done. Please note that you'll need to relog to see a difference in-game.`, - code_nothingToDo: `[UNTRANSLATED] Done. There was nothing to do.`, + code_succRelog: `Hecho. Ten en cuenta que deberás volver a iniciar sesión para ver los cambios en el juego.`, + code_nothingToDo: `Hecho. No había nada que hacer.`, code_gild: `Refinar`, code_moa: `Moa`, code_zanuka: `Sabueso`, @@ -129,7 +129,7 @@ dict = { mods_fingerprintHelp: `¿Necesitas ayuda con la huella digital?`, mods_rivens: `Agrietados`, mods_mods: `Mods`, - mods_addMax: `[UNTRANSLATED] Add Maxed`, + mods_addMax: `Agregar al máximo`, mods_addMissingUnrankedMods: `Agregar mods sin rango faltantes`, mods_removeUnranked: `Quitar mods sin rango`, mods_addMissingMaxRankMods: `Agregar mods de rango máximo faltantes`, @@ -185,13 +185,13 @@ dict = { cheats_noDojoResearchTime: `Sin tiempo de investigación del dojo`, cheats_fastClanAscension: `Ascenso rápido del clan`, cheats_missionsCanGiveAllRelics: `Las misiones pueden otorgar todas las reliquias`, - cheats_exceptionalRelicsAlwaysGiveBronzeReward: `[UNTRANSLATED] Exceptional Relics Always Give Bronze Reward`, - cheats_flawlessRelicsAlwaysGiveSilverReward: `[UNTRANSLATED] Flawless Relics Always Give Silver Reward`, - cheats_radiantRelicsAlwaysGiveGoldReward: `[UNTRANSLATED] Radiant Relics Always Give Gold Reward`, + cheats_exceptionalRelicsAlwaysGiveBronzeReward: `Las reliquias excepcionales siempre otorgan recompensa de bronce`, + cheats_flawlessRelicsAlwaysGiveSilverReward: `Las reliquias impecables siempre otorgan recompensa de plata`, + cheats_radiantRelicsAlwaysGiveGoldReward: `Las reliquias radiantes siempre otorgan recompensa de oro`, cheats_unlockAllSimarisResearchEntries: `Desbloquear todas las entradas de investigación de Simaris`, cheats_disableDailyTribute: `Desactivar tributo diario`, cheats_spoofMasteryRank: `Rango de maestría simulado (-1 para desactivar)`, - cheats_relicRewardItemCountMultiplier: `[UNTRANSLATED] Relic Reward Item Count Multiplier`, + cheats_relicRewardItemCountMultiplier: `Multiplicador de cantidad de recompensas de reliquia`, cheats_nightwaveStandingMultiplier: `Multiplicador de Reputación de Onda Nocturna`, cheats_save: `Guardar`, cheats_account: `Cuenta`, @@ -202,7 +202,7 @@ dict = { cheats_changeSupportedSyndicate: `Sindicatos disponibles`, cheats_changeButton: `Cambiar`, cheats_none: `Ninguno`, - cheats_markAllAsRead: `[UNTRANSLATED] Mark Inbox As Read`, + cheats_markAllAsRead: `Marcar bandeja de entrada como leída`, worldState: `Estado del mundo`, worldState_creditBoost: `Potenciador de Créditos`, @@ -249,8 +249,8 @@ dict = { worldState_allAtOnceSteelPath: `Todo a la vez, Camino de Acero`, worldState_theCircuitOverride: `Cambio del Circuito`, worldState_darvoStockMultiplier: `Multiplicador de stock de Darvo`, - worldState_varziaFullyStocked: `[UNTRANSLATED] Varzia Fully Stocked`, - worldState_varziaOverride: `[UNTRANSLATED] Varzia Rotation Override`, + worldState_varziaFullyStocked: `Varzia con stock completo`, + worldState_varziaOverride: `Cambio en rotación de Varzia`, import_importNote: `Puedes proporcionar una respuesta de inventario completa o parcial (representación del cliente) aquí. Todos los campos compatibles con el importador serán sobrescritos en tu cuenta.`, import_submit: `Enviar`, @@ -303,10 +303,10 @@ dict = { upgrade_OnExecutionTerrify: `50% de probabilidad de que enemigos en un radio de 15m entren en pánico por 8s tras una ejecución`, upgrade_OnHackLockers: `Desbloquea 5 casilleros en un radio de 20m tras hackear`, upgrade_OnExecutionBlind: `Ciega a los enemigos en un radio de 18m tras una ejecución`, - upgrade_OnExecutionDrainPower: `[UNTRANSLATED] Next ability cast gains +50% Ability Strength on Mercy`, + upgrade_OnExecutionDrainPower: `La próxima habilidad usada gana +50% de fuerza al realizar un remate (Mercy)`, upgrade_OnHackSprintSpeed: `+75% de velocidad de carrera durante 15s después de hackear`, - upgrade_SwiftExecute: `[UNTRANSLATED] +50% Mercy Kill Speed`, - upgrade_OnHackInvis: `[UNTRANSLATED] Invisible for 15 seconds after Hacking`, + upgrade_SwiftExecute: `+50% de velocidad al ejecutar remates (Mercy)`, + upgrade_OnHackInvis: `Invisible durante 15 segundos después de hackear`, damageType_Electricity: `Eletricidade`, damageType_Fire: `Ígneo`, From e18b8e09eaa4de45678471806d59b452882011ea Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 10 Jul 2025 20:59:32 -0700 Subject: [PATCH 5/6] fix: properly track xp for modular items (#2460) Closes #2454 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2460 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/services/inventoryService.ts | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/services/inventoryService.ts b/src/services/inventoryService.ts index 766ee4b1..30db6edc 100644 --- a/src/services/inventoryService.ts +++ b/src/services/inventoryService.ts @@ -1635,6 +1635,15 @@ export const addEmailItem = async ( return inventoryChanges; }; +const xpEarningParts: readonly string[] = [ + "LWPT_BLADE", + "LWPT_GUN_BARREL", + "LWPT_AMP_OCULUS", + "LWPT_MOA_HEAD", + "LWPT_ZANUKA_HEAD", + "LWPT_HB_DECK" +]; + export const applyClientEquipmentUpdates = ( inventory: TInventoryDatabaseDocument, gearArray: IEquipmentClient[], @@ -1653,13 +1662,26 @@ export const applyClientEquipmentUpdates = ( item.XP ??= 0; item.XP += XP; - const xpinfoIndex = inventory.XPInfo.findIndex(x => x.ItemType == item.ItemType); + let xpItemType = item.ItemType; + if (item.ModularParts) { + for (const part of item.ModularParts) { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + const partType = ExportWeapons[part]?.partType; + if (partType !== undefined && xpEarningParts.indexOf(partType) != -1) { + xpItemType = part; + break; + } + } + logger.debug(`adding xp to ${xpItemType} for modular item ${fromOid(ItemId)} (${item.ItemType})`); + } + + const xpinfoIndex = inventory.XPInfo.findIndex(x => x.ItemType == xpItemType); if (xpinfoIndex !== -1) { const xpinfo = inventory.XPInfo[xpinfoIndex]; xpinfo.XP += XP; } else { inventory.XPInfo.push({ - ItemType: item.ItemType, + ItemType: xpItemType, XP: XP }); } From f796f9a85168b4bbd8d2cedee9e25a2894f87998 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Thu, 10 Jul 2025 20:59:39 -0700 Subject: [PATCH 6/6] feat: resetQuestProgress (#2461) Just giving the client an 'ok' response. It seems that it does use updateQuest to manage the state itself mostly, just the server and webui are a bit confused about a quest with all stages completed still being active. Re #1323 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2461 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/resetQuestProgressController.ts | 5 +++++ src/routes/api.ts | 2 ++ 2 files changed, 7 insertions(+) create mode 100644 src/controllers/api/resetQuestProgressController.ts diff --git a/src/controllers/api/resetQuestProgressController.ts b/src/controllers/api/resetQuestProgressController.ts new file mode 100644 index 00000000..9d75a54a --- /dev/null +++ b/src/controllers/api/resetQuestProgressController.ts @@ -0,0 +1,5 @@ +import { RequestHandler } from "express"; + +export const resetQuestProgressController: RequestHandler = (_req, res) => { + res.send("1").end(); +}; diff --git a/src/routes/api.ts b/src/routes/api.ts index 2a3255cf..1ce15825 100644 --- a/src/routes/api.ts +++ b/src/routes/api.ts @@ -112,6 +112,7 @@ import { removeFromGuildController } from "@/src/controllers/api/removeFromGuild import { removeIgnoredUserController } from "@/src/controllers/api/removeIgnoredUserController"; import { renamePetController } from "@/src/controllers/api/renamePetController"; import { rerollRandomModController } from "@/src/controllers/api/rerollRandomModController"; +import { resetQuestProgressController } from "@/src/controllers/api/resetQuestProgressController"; import { retrievePetFromStasisController } from "@/src/controllers/api/retrievePetFromStasisController"; import { saveDialogueController } from "@/src/controllers/api/saveDialogueController"; import { saveLoadoutController } from "@/src/controllers/api/saveLoadoutController"; @@ -209,6 +210,7 @@ apiRouter.get("/questControl.php", questControlController); apiRouter.get("/queueDojoComponentDestruction.php", queueDojoComponentDestructionController); apiRouter.get("/removeFriend.php", removeFriendGetController); apiRouter.get("/removeFromAlliance.php", removeFromAllianceController); +apiRouter.get("/resetQuestProgress.php", resetQuestProgressController); apiRouter.get("/setActiveQuest.php", setActiveQuestController); apiRouter.get("/setActiveShip.php", setActiveShipController); apiRouter.get("/setAllianceGuildPermissions.php", setAllianceGuildPermissionsController);