From ca13c3fc81bd6e9b0eb8480f3df85a11ca0b690b Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Sun, 14 Sep 2025 12:15:42 +0200 Subject: [PATCH] fix: handle quest replay Closes #2496 --- .../giveKeyChainTriggeredMessageController.ts | 4 +- .../custom/manageQuestsController.ts | 32 ++-- src/services/questService.ts | 157 ++++++++++-------- src/types/inventoryTypes/inventoryTypes.ts | 8 +- static/webui/script.js | 7 +- static/webui/translations/de.js | 1 + static/webui/translations/en.js | 1 + static/webui/translations/es.js | 1 + static/webui/translations/fr.js | 1 + static/webui/translations/ru.js | 1 + static/webui/translations/uk.js | 1 + static/webui/translations/zh.js | 1 + 12 files changed, 131 insertions(+), 84 deletions(-) diff --git a/src/controllers/api/giveKeyChainTriggeredMessageController.ts b/src/controllers/api/giveKeyChainTriggeredMessageController.ts index e4d243be..6df1d8aa 100644 --- a/src/controllers/api/giveKeyChainTriggeredMessageController.ts +++ b/src/controllers/api/giveKeyChainTriggeredMessageController.ts @@ -8,8 +8,8 @@ export const giveKeyChainTriggeredMessageController: RequestHandler = async (req const accountId = await getAccountIdForRequest(req); const keyChainInfo = JSON.parse((req.body as Buffer).toString()) as IKeyChainRequest; - const inventory = await getInventory(accountId, "QuestKeys"); - await giveKeyChainMessage(inventory, accountId, keyChainInfo); + const inventory = await getInventory(accountId, "QuestKeys accountOwnerId"); + await giveKeyChainMessage(inventory, keyChainInfo); await inventory.save(); res.send(1); diff --git a/src/controllers/custom/manageQuestsController.ts b/src/controllers/custom/manageQuestsController.ts index b1cbde84..efd1ec63 100644 --- a/src/controllers/custom/manageQuestsController.ts +++ b/src/controllers/custom/manageQuestsController.ts @@ -102,8 +102,16 @@ export const manageQuestsController: RequestHandler = async (req, res) => { questKey.Completed = false; questKey.CompletionDate = undefined; } - questKey.Progress.pop(); - const stage = questKey.Progress.length - 1; + + const run = questKey.Progress[0]?.c ?? 0; + const stage = questKey.Progress.map(p => p.c).lastIndexOf(run); + + if (run > 0) { + questKey.Progress[stage].c = run - 1; + } else { + questKey.Progress.pop(); + } + if (stage > 0) { await giveKeyChainStageTriggered(inventory, { KeyChain: questKey.ItemType, @@ -123,28 +131,28 @@ export const manageQuestsController: RequestHandler = async (req, res) => { } if (!questKey.Progress) break; - const currentStage = questKey.Progress.length; + const run = questKey.Progress[0]?.c ?? 0; + const currentStage = questKey.Progress.map(p => p.c).lastIndexOf(run); + if (currentStage + 1 == questManifest.chainStages?.length) { logger.debug(`Trying to complete last stage with nextStage, calling completeQuest instead`); await completeQuest(inventory, questKey.ItemType); } else { - const progress = { - c: 0, - i: false, - m: false, - b: [] - }; - questKey.Progress.push(progress); + if (run > 0) { + questKey.Progress[currentStage + 1].c = run; + } else { + questKey.Progress.push({ c: run, i: false, m: false, b: [] }); + } await giveKeyChainStageTriggered(inventory, { KeyChain: questKey.ItemType, - ChainStage: currentStage + ChainStage: currentStage + 1 }); if (currentStage > 0) { await giveKeyChainMissionReward(inventory, { KeyChain: questKey.ItemType, - ChainStage: currentStage - 1 + ChainStage: currentStage }); } } diff --git a/src/services/questService.ts b/src/services/questService.ts index f79eb131..d0a4a468 100644 --- a/src/services/questService.ts +++ b/src/services/questService.ts @@ -6,7 +6,6 @@ import { addItem, addItems, addKeyChainItems, setupKahlSyndicate } from "./inven import { fromStoreItem, getKeyChainMessage, getLevelKeyRewards } from "./itemDataService.ts"; import type { IQuestKeyClient, IQuestKeyDatabase, IQuestStage } from "../types/inventoryTypes/inventoryTypes.ts"; import { logger } from "../utils/logger.ts"; -import type { Types } from "mongoose"; import { ExportKeys } from "warframe-public-export-plus"; import { addFixedLevelRewards } from "./missionInventoryUpdateService.ts"; import type { IInventoryChanges } from "../types/purchaseTypes.ts"; @@ -44,7 +43,12 @@ export const updateQuestKey = async ( inventory.QuestKeys[questKeyIndex].CompletionDate = new Date(); const questKey = questKeyUpdate[0].ItemType; - await handleQuestCompletion(inventory, questKey, inventoryChanges); + await handleQuestCompletion( + inventory, + questKey, + inventoryChanges, + (questKeyUpdate[0].Progress?.[0]?.c ?? 0) > 0 + ); } return inventoryChanges; }; @@ -52,7 +56,7 @@ export const updateQuestKey = async ( export const updateQuestStage = ( inventory: TInventoryDatabaseDocument, { KeyChain, ChainStage }: IKeyChainRequest, - questStageUpdate: IQuestStage + questStageUpdate: Partial ): void => { const quest = inventory.QuestKeys.find(quest => quest.ItemType === KeyChain); @@ -68,14 +72,22 @@ export const updateQuestStage = ( // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!questStage) { - const questStageIndex = quest.Progress.push(questStageUpdate) - 1; + const questStageIndex = + quest.Progress.push({ + c: questStageUpdate.c ?? 0, + i: questStageUpdate.i ?? false, + m: questStageUpdate.m ?? false, + b: questStageUpdate.b ?? [] + }) - 1; if (questStageIndex !== ChainStage) { throw new Error(`Quest stage index mismatch: ${questStageIndex} !== ${ChainStage}`); } return; } - Object.assign(questStage, questStageUpdate); + for (const [key, value] of Object.entries(questStageUpdate) as [keyof IQuestStage, number | boolean | any[]][]) { + (questStage[key] as any) = value; + } }; export const addQuestKey = ( @@ -112,58 +124,53 @@ export const completeQuest = async (inventory: TInventoryDatabaseDocument, quest } const chainStageTotal = chainStages.length; + let existingQuestKey = inventory.QuestKeys.find(qk => qk.ItemType === questKey); - const existingQuestKey = inventory.QuestKeys.find(qk => qk.ItemType === questKey); - - const startingStage = Math.max((existingQuestKey?.Progress?.length ?? 0) - 1, 0); - - if (existingQuestKey?.Completed) { - return; - } - if (existingQuestKey) { - existingQuestKey.Progress = existingQuestKey.Progress ?? []; - - const existingProgressLength = existingQuestKey.Progress.length; - - if (existingProgressLength < chainStageTotal) { - const missingProgress: IQuestStage[] = Array.from( - { length: chainStageTotal - existingProgressLength }, - () => - ({ - c: 0, - i: false, - m: false, - b: [] - }) as IQuestStage - ); - - existingQuestKey.Progress.push(...missingProgress); - existingQuestKey.CompletionDate = new Date(); - existingQuestKey.Completed = true; - } - } else { + if (!existingQuestKey) { const completedQuestKey: IQuestKeyDatabase = { ItemType: questKey, - Completed: true, + Completed: false, unlock: true, - Progress: Array(chainStageTotal).fill({ + Progress: Array.from({ length: chainStageTotal }, () => ({ c: 0, i: false, m: false, b: [] - } satisfies IQuestStage), - CompletionDate: new Date() + })) }; addQuestKey(inventory, completedQuestKey); + existingQuestKey = inventory.QuestKeys.find(qk => qk.ItemType === questKey)!; + } else if (existingQuestKey.Completed) { + return; } - for (let i = startingStage; i < chainStageTotal; i++) { - await giveKeyChainStageTriggered(inventory, { KeyChain: questKey, ChainStage: i }); + existingQuestKey.Progress = existingQuestKey.Progress ?? []; - await giveKeyChainMissionReward(inventory, { KeyChain: questKey, ChainStage: i }); + const run = existingQuestKey.Progress[0]?.c ?? 0; + + const existingProgressLength = existingQuestKey.Progress.length; + if (existingProgressLength < chainStageTotal) { + const missingProgress: IQuestStage[] = Array.from( + { length: chainStageTotal - existingProgressLength }, + () => ({ c: run, i: false, m: false, b: [] }) as IQuestStage + ); + existingQuestKey.Progress.push(...missingProgress); } - await handleQuestCompletion(inventory, questKey); + for (let i = 0; i < chainStageTotal; i++) { + const stage = existingQuestKey.Progress[i]; + if (stage.c < run) { + stage.c = run; + await giveKeyChainStageTriggered(inventory, { KeyChain: questKey, ChainStage: i }); + await giveKeyChainMissionReward(inventory, { KeyChain: questKey, ChainStage: i }); + } + } + + if (existingQuestKey.Progress.every(p => p.c == run)) { + existingQuestKey.Completed = true; + existingQuestKey.CompletionDate = new Date(); + await handleQuestCompletion(inventory, questKey, undefined, run > 0); + } }; const getQuestCompletionItems = (questKey: string): ITypeCount[] | undefined => { @@ -214,28 +221,35 @@ const doesQuestCompletionFinishSet = ( const handleQuestCompletion = async ( inventory: TInventoryDatabaseDocument, questKey: string, - inventoryChanges: IInventoryChanges = {} + inventoryChanges: IInventoryChanges = {}, + isRerun: boolean = false ): Promise => { logger.debug(`completed quest ${questKey}`); + if (inventory.ActiveQuest == questKey) inventory.ActiveQuest = ""; if (questKey == "/Lotus/Types/Keys/OrokinMoonQuest/OrokinMoonQuestKeyChain") { + const att = isRerun + ? [] + : [ + "/Lotus/Weapons/Tenno/Melee/Swords/StalkerTwo/StalkerTwoSmallSword", + "/Lotus/Upgrades/Skins/Sigils/ScarSigil" + ]; await createMessage(inventory.accountOwnerId, [ { sndr: "/Lotus/Language/Bosses/Ordis", msg: "/Lotus/Language/G1Quests/SecondDreamFinishInboxMessage", - att: [ - "/Lotus/Weapons/Tenno/Melee/Swords/StalkerTwo/StalkerTwoSmallSword", - "/Lotus/Upgrades/Skins/Sigils/ScarSigil" - ], + att, sub: "/Lotus/Language/G1Quests/SecondDreamFinishInboxTitle", icon: "/Lotus/Interface/Icons/Npcs/Ordis.png", highPriority: true } ]); - } else if (questKey == "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain") { + } else if (questKey == "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain" && !isRerun) { setupKahlSyndicate(inventory); } + if (isRerun) return; + // Whispers in the Walls is unlocked once The New War + Heart of Deimos are completed. if ( doesQuestCompletionFinishSet(inventory, questKey, [ @@ -279,21 +293,24 @@ const handleQuestCompletion = async ( if (questCompletionItems) { await addItems(inventory, questCompletionItems, inventoryChanges); } - - if (inventory.ActiveQuest == questKey) inventory.ActiveQuest = ""; }; export const giveKeyChainItem = async ( inventory: TInventoryDatabaseDocument, - keyChainInfo: IKeyChainRequest + keyChainInfo: IKeyChainRequest, + isRerun: boolean = false ): Promise => { - const inventoryChanges = await addKeyChainItems(inventory, keyChainInfo); + let inventoryChanges: IInventoryChanges = {}; - if (isEmptyObject(inventoryChanges)) { - logger.warn("inventory changes was empty after getting keychain items: should not happen"); + if (!isRerun) { + inventoryChanges = await addKeyChainItems(inventory, keyChainInfo); + + if (isEmptyObject(inventoryChanges)) { + logger.warn("inventory changes was empty after getting keychain items: should not happen"); + } + // items were added: update quest stage's i (item was given) + updateQuestStage(inventory, keyChainInfo, { i: true }); } - // items were added: update quest stage's i (item was given) - updateQuestStage(inventory, keyChainInfo, { i: true }); return inventoryChanges; @@ -309,12 +326,17 @@ export const giveKeyChainItem = async ( export const giveKeyChainMessage = async ( inventory: TInventoryDatabaseDocument, - accountId: string | Types.ObjectId, - keyChainInfo: IKeyChainRequest + keyChainInfo: IKeyChainRequest, + isRerun: boolean = false ): Promise => { const keyChainMessage = getKeyChainMessage(keyChainInfo); - await createMessage(accountId, [keyChainMessage]); + if (!isRerun) { + keyChainMessage.att = []; + keyChainMessage.countedAtt = []; + } + + await createMessage(inventory.accountOwnerId, [keyChainMessage]); updateQuestStage(inventory, keyChainInfo, { m: true }); }; @@ -328,8 +350,10 @@ export const giveKeyChainMissionReward = async ( if (chainStages) { const missionName = chainStages[keyChainInfo.ChainStage].key; - if (missionName) { + const questKey = inventory.QuestKeys.find(q => q.ItemType === keyChainInfo.KeyChain); + if (missionName && questKey) { const fixedLevelRewards = getLevelKeyRewards(missionName); + const run = questKey.Progress?.[0]?.c ?? 0; if (fixedLevelRewards.levelKeyRewards) { const missionRewards: { StoreItem: string; ItemCount: number }[] = []; inventory.RegularCredits += addFixedLevelRewards(fixedLevelRewards.levelKeyRewards, missionRewards); @@ -338,7 +362,7 @@ export const giveKeyChainMissionReward = async ( await addItem(inventory, fromStoreItem(reward.StoreItem), reward.ItemCount); } - updateQuestStage(inventory, keyChainInfo, { c: 0 }); + updateQuestStage(inventory, keyChainInfo, { c: run }); } else if (fixedLevelRewards.levelKeyRewards2) { for (const reward of fixedLevelRewards.levelKeyRewards2) { if (reward.rewardType == "RT_CREDITS") { @@ -352,7 +376,7 @@ export const giveKeyChainMissionReward = async ( } } - updateQuestStage(inventory, keyChainInfo, { c: 0 }); + updateQuestStage(inventory, keyChainInfo, { c: run }); } } } @@ -364,14 +388,17 @@ export const giveKeyChainStageTriggered = async ( ): Promise => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition const chainStages = ExportKeys[keyChainInfo.KeyChain]?.chainStages; + const questKey = inventory.QuestKeys.find(qk => qk.ItemType === keyChainInfo.KeyChain); + + if (chainStages && questKey) { + const run = questKey.Progress?.[0]?.c ?? 0; - if (chainStages) { if (chainStages[keyChainInfo.ChainStage].itemsToGiveWhenTriggered.length > 0) { - await giveKeyChainItem(inventory, keyChainInfo); + await giveKeyChainItem(inventory, keyChainInfo, run > 0); } if (chainStages[keyChainInfo.ChainStage].messageToSendWhenTriggered) { - await giveKeyChainMessage(inventory, inventory.accountOwnerId, keyChainInfo); + await giveKeyChainMessage(inventory, keyChainInfo, run > 0); } } }; diff --git a/src/types/inventoryTypes/inventoryTypes.ts b/src/types/inventoryTypes/inventoryTypes.ts index df71f392..83549b52 100644 --- a/src/types/inventoryTypes/inventoryTypes.ts +++ b/src/types/inventoryTypes/inventoryTypes.ts @@ -977,10 +977,10 @@ export interface IQuestKeyClient extends Omit { const tr = document.createElement("tr"); tr.setAttribute("data-item-type", item.ItemType); - const stage = item.Progress?.length ?? 0; + const run = item.Progress[0]?.c ?? 0; + const stage = run == 0 ? item.Progress.length : item.Progress.map(p => p.c ?? 0).lastIndexOf(run); const datalist = document.getElementById("datalist-QuestKeys"); const optionToRemove = datalist.querySelector(`option[data-key="${item.ItemType}"]`); @@ -1007,6 +1008,10 @@ function updateInventory() { td.textContent += " | " + loc("code_completed"); } + if (run > 0) { + td.textContent += " | " + loc("code_replays") + ": " + (run + 1); + } + if (data.ActiveQuest == item.ItemType) td.textContent += " | " + loc("code_active"); tr.appendChild(td); } diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index d370142c..c049fe17 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -68,6 +68,7 @@ dict = { code_unmature: `Genetisches Altern zurücksetzen`, code_fund: `[UNTRANSLATED] Fund`, code_funded: `[UNTRANSLATED] Funded`, + code_replays: `[UNTRANSLATED] Replays`, code_succChange: `Erfolgreich geändert.`, code_requiredInvigorationUpgrade: `Du musst sowohl ein offensives & defensives Upgrade auswählen.`, login_description: `Melde dich mit deinem OpenWF-Account an (denselben Angaben wie im Spiel, wenn du dich mit diesem Server verbindest).`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 5f82beca..93805d99 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -67,6 +67,7 @@ dict = { code_unmature: `Regress genetic aging`, code_fund: `Fund`, code_funded: `Funded`, + code_replays: `Replays`, code_succChange: `Successfully changed.`, code_requiredInvigorationUpgrade: `You must select both an offensive & defensive upgrade.`, login_description: `Login using your OpenWF account credentials (same as in-game when connecting to this server).`, diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index 337cc27a..eac665ac 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -68,6 +68,7 @@ dict = { code_unmature: `Regresar el envejecimiento genético`, code_fund: `[UNTRANSLATED] Fund`, code_funded: `[UNTRANSLATED] Funded`, + code_replays: `[UNTRANSLATED] Replays`, code_succChange: `Cambiado correctamente`, code_requiredInvigorationUpgrade: `Debes seleccionar una mejora ofensiva y una defensiva.`, login_description: `Inicia sesión con las credenciales de tu cuenta OpenWF (las mismas que usas en el juego al conectarte a este servidor).`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index 15d4be30..44738afa 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -68,6 +68,7 @@ dict = { code_unmature: `Régrésser l'âge génétique`, code_fund: `Financer`, code_funded: `Complété`, + code_replays: `[UNTRANSLATED] Replays`, code_succChange: `Changement effectué.`, code_requiredInvigorationUpgrade: `Augmentation offensive et défensive requises.`, login_description: `Connexion avec les informations de connexion OpenWF.`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index c1157079..b836a40c 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -68,6 +68,7 @@ dict = { code_unmature: `Регрессия генетического старения`, code_fund: `Профинансировать`, code_funded: `Профинансировано`, + code_replays: `Повторов`, code_succChange: `Успешно изменено.`, code_requiredInvigorationUpgrade: `Вы должны выбрать как атакующее, так и вспомогательное улучшение.`, login_description: `Войдите, используя учетные данные OpenWF (те же, что и в игре при подключении к этому серверу).`, diff --git a/static/webui/translations/uk.js b/static/webui/translations/uk.js index 7981eae1..60a021ab 100644 --- a/static/webui/translations/uk.js +++ b/static/webui/translations/uk.js @@ -68,6 +68,7 @@ dict = { code_unmature: `Обернути старіння`, code_fund: `[UNTRANSLATED] Fund`, code_funded: `[UNTRANSLATED] Funded`, + code_replays: `[UNTRANSLATED] Replays`, code_succChange: `Успішно змінено.`, code_requiredInvigorationUpgrade: `Ви повинні вибрати як атакуюче, так і допоміжне вдосконалення.`, login_description: `Увійдіть, використовуючи облікові дані OpenWF (ті ж, що й у грі при підключенні до цього серверу).`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index 4b806799..46a3ecc8 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -68,6 +68,7 @@ dict = { code_unmature: `逆转衰老基因`, code_fund: `[UNTRANSLATED] Fund`, code_funded: `[UNTRANSLATED] Funded`, + code_replays: `[UNTRANSLATED] Replays`, code_succChange: `更改成功`, code_requiredInvigorationUpgrade: `您必须同时选择一个进攻型和一个功能型活化属性.`, login_description: `使用您的 OpenWF 账户凭证登录(与游戏内连接本服务器时使用的昵称相同)`, -- 2.47.2