XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

SpaceNinjaServer

A simple server for a small space ninja game

公开
关注 0 Fork 1 Star 0
返回提交历史

XFEstudio/SpaceNinjaServer

fix: handle quest replay (#2778)

Closes #2496 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2778 Reviewed-by: Sainan <63328889+sainan@users.noreply.github.com> Co-authored-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Co-committed-by: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>

62570177
AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com>
提交于

代码差异

12 个文件 +131 -84
Modified src/controllers/api/giveKeyChainTriggeredMessageController.ts +2 -2
@@ -8,8 +8,8 @@ export const giveKeyChainTriggeredMessageController: RequestHandler = async (req
8 8 const accountId = await getAccountIdForRequest(req);
9 9 const keyChainInfo = JSON.parse((req.body as Buffer).toString()) as IKeyChainRequest;
10 10
11 const inventory = await getInventory(accountId, "QuestKeys");
12 await giveKeyChainMessage(inventory, accountId, keyChainInfo);
11 const inventory = await getInventory(accountId, "QuestKeys accountOwnerId");
12 await giveKeyChainMessage(inventory, keyChainInfo);
13 13 await inventory.save();
14 14
15 15 res.send(1);
Modified src/controllers/custom/manageQuestsController.ts +20 -12
@@ -102,8 +102,16 @@ export const manageQuestsController: RequestHandler = async (req, res) => {
102 102 questKey.Completed = false;
103 103 questKey.CompletionDate = undefined;
104 104 }
105 questKey.Progress.pop();
106 const stage = questKey.Progress.length - 1;
105
106 const run = questKey.Progress[0]?.c ?? 0;
107 const stage = questKey.Progress.map(p => p.c).lastIndexOf(run);
108
109 if (run > 0) {
110 questKey.Progress[stage].c = run - 1;
111 } else {
112 questKey.Progress.pop();
113 }
114
107 115 if (stage > 0) {
108 116 await giveKeyChainStageTriggered(inventory, {
109 117 KeyChain: questKey.ItemType,
@@ -123,28 +131,28 @@ export const manageQuestsController: RequestHandler = async (req, res) => {
123 131 }
124 132 if (!questKey.Progress) break;
125 133
126 const currentStage = questKey.Progress.length;
134 const run = questKey.Progress[0]?.c ?? 0;
135 const currentStage = questKey.Progress.map(p => p.c).lastIndexOf(run);
136
127 137 if (currentStage + 1 == questManifest.chainStages?.length) {
128 138 logger.debug(`Trying to complete last stage with nextStage, calling completeQuest instead`);
129 139 await completeQuest(inventory, questKey.ItemType);
130 140 } else {
131 const progress = {
132 c: 0,
133 i: false,
134 m: false,
135 b: []
136 };
137 questKey.Progress.push(progress);
141 if (run > 0) {
142 questKey.Progress[currentStage + 1].c = run;
143 } else {
144 questKey.Progress.push({ c: run, i: false, m: false, b: [] });
145 }
138 146
139 147 await giveKeyChainStageTriggered(inventory, {
140 148 KeyChain: questKey.ItemType,
141 ChainStage: currentStage
149 ChainStage: currentStage + 1
142 150 });
143 151
144 152 if (currentStage > 0) {
145 153 await giveKeyChainMissionReward(inventory, {
146 154 KeyChain: questKey.ItemType,
147 ChainStage: currentStage - 1
155 ChainStage: currentStage
148 156 });
149 157 }
150 158 }
Modified src/services/questService.ts +92 -65
@@ -6,7 +6,6 @@ import { addItem, addItems, addKeyChainItems, setupKahlSyndicate } from "./inven
6 6 import { fromStoreItem, getKeyChainMessage, getLevelKeyRewards } from "./itemDataService.ts";
7 7 import type { IQuestKeyClient, IQuestKeyDatabase, IQuestStage } from "../types/inventoryTypes/inventoryTypes.ts";
8 8 import { logger } from "../utils/logger.ts";
9 import type { Types } from "mongoose";
10 9 import { ExportKeys } from "warframe-public-export-plus";
11 10 import { addFixedLevelRewards } from "./missionInventoryUpdateService.ts";
12 11 import type { IInventoryChanges } from "../types/purchaseTypes.ts";
@@ -44,7 +43,12 @@ export const updateQuestKey = async (
44 43 inventory.QuestKeys[questKeyIndex].CompletionDate = new Date();
45 44
46 45 const questKey = questKeyUpdate[0].ItemType;
47 await handleQuestCompletion(inventory, questKey, inventoryChanges);
46 await handleQuestCompletion(
47 inventory,
48 questKey,
49 inventoryChanges,
50 (questKeyUpdate[0].Progress?.[0]?.c ?? 0) > 0
51 );
48 52 }
49 53 return inventoryChanges;
50 54 };
@@ -52,7 +56,7 @@ export const updateQuestKey = async (
52 56 export const updateQuestStage = (
53 57 inventory: TInventoryDatabaseDocument,
54 58 { KeyChain, ChainStage }: IKeyChainRequest,
55 questStageUpdate: IQuestStage
59 questStageUpdate: Partial<IQuestStage>
56 60 ): void => {
57 61 const quest = inventory.QuestKeys.find(quest => quest.ItemType === KeyChain);
58 62
@@ -68,14 +72,22 @@ export const updateQuestStage = (
68 72
69 73 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
70 74 if (!questStage) {
71 const questStageIndex = quest.Progress.push(questStageUpdate) - 1;
75 const questStageIndex =
76 quest.Progress.push({
77 c: questStageUpdate.c ?? 0,
78 i: questStageUpdate.i ?? false,
79 m: questStageUpdate.m ?? false,
80 b: questStageUpdate.b ?? []
81 }) - 1;
72 82 if (questStageIndex !== ChainStage) {
73 83 throw new Error(`Quest stage index mismatch: ${questStageIndex} !== ${ChainStage}`);
74 84 }
75 85 return;
76 86 }
77 87
78 Object.assign(questStage, questStageUpdate);
88 for (const [key, value] of Object.entries(questStageUpdate) as [keyof IQuestStage, number | boolean | any[]][]) {
89 (questStage[key] as any) = value;
90 }
79 91 };
80 92
81 93 export const addQuestKey = (
@@ -112,58 +124,53 @@ export const completeQuest = async (inventory: TInventoryDatabaseDocument, quest
112 124 }
113 125
114 126 const chainStageTotal = chainStages.length;
127 let existingQuestKey = inventory.QuestKeys.find(qk => qk.ItemType === questKey);
115 128
116 const existingQuestKey = inventory.QuestKeys.find(qk => qk.ItemType === questKey);
117
118 const startingStage = Math.max((existingQuestKey?.Progress?.length ?? 0) - 1, 0);
119
120 if (existingQuestKey?.Completed) {
121 return;
122 }
123 if (existingQuestKey) {
124 existingQuestKey.Progress = existingQuestKey.Progress ?? [];
125
126 const existingProgressLength = existingQuestKey.Progress.length;
127
128 if (existingProgressLength < chainStageTotal) {
129 const missingProgress: IQuestStage[] = Array.from(
130 { length: chainStageTotal - existingProgressLength },
131 () =>
132 ({
133 c: 0,
134 i: false,
135 m: false,
136 b: []
137 }) as IQuestStage
138 );
139
140 existingQuestKey.Progress.push(...missingProgress);
141 existingQuestKey.CompletionDate = new Date();
142 existingQuestKey.Completed = true;
143 }
144 } else {
129 if (!existingQuestKey) {
145 130 const completedQuestKey: IQuestKeyDatabase = {
146 131 ItemType: questKey,
147 Completed: true,
132 Completed: false,
148 133 unlock: true,
149 Progress: Array(chainStageTotal).fill({
134 Progress: Array.from({ length: chainStageTotal }, () => ({
150 135 c: 0,
151 136 i: false,
152 137 m: false,
153 138 b: []
154 } satisfies IQuestStage),
155 CompletionDate: new Date()
139 }))
156 140 };
157 141 addQuestKey(inventory, completedQuestKey);
142 existingQuestKey = inventory.QuestKeys.find(qk => qk.ItemType === questKey)!;
143 } else if (existingQuestKey.Completed) {
144 return;
158 145 }
159 146
160 for (let i = startingStage; i < chainStageTotal; i++) {
161 await giveKeyChainStageTriggered(inventory, { KeyChain: questKey, ChainStage: i });
147 existingQuestKey.Progress = existingQuestKey.Progress ?? [];
148
149 const run = existingQuestKey.Progress[0]?.c ?? 0;
150
151 const existingProgressLength = existingQuestKey.Progress.length;
152 if (existingProgressLength < chainStageTotal) {
153 const missingProgress: IQuestStage[] = Array.from(
154 { length: chainStageTotal - existingProgressLength },
155 () => ({ c: run, i: false, m: false, b: [] }) as IQuestStage
156 );
157 existingQuestKey.Progress.push(...missingProgress);
158 }
162 159
163 await giveKeyChainMissionReward(inventory, { KeyChain: questKey, ChainStage: i });
160 for (let i = 0; i < chainStageTotal; i++) {
161 const stage = existingQuestKey.Progress[i];
162 if (stage.c < run) {
163 stage.c = run;
164 await giveKeyChainStageTriggered(inventory, { KeyChain: questKey, ChainStage: i });
165 await giveKeyChainMissionReward(inventory, { KeyChain: questKey, ChainStage: i });
166 }
164 167 }
165 168
166 await handleQuestCompletion(inventory, questKey);
169 if (existingQuestKey.Progress.every(p => p.c == run)) {
170 existingQuestKey.Completed = true;
171 existingQuestKey.CompletionDate = new Date();
172 await handleQuestCompletion(inventory, questKey, undefined, run > 0);
173 }
167 174 };
168 175
169 176 const getQuestCompletionItems = (questKey: string): ITypeCount[] | undefined => {
@@ -214,28 +221,35 @@ const doesQuestCompletionFinishSet = (
214 221 const handleQuestCompletion = async (
215 222 inventory: TInventoryDatabaseDocument,
216 223 questKey: string,
217 inventoryChanges: IInventoryChanges = {}
224 inventoryChanges: IInventoryChanges = {},
225 isRerun: boolean = false
218 226 ): Promise<void> => {
219 227 logger.debug(`completed quest ${questKey}`);
220 228
229 if (inventory.ActiveQuest == questKey) inventory.ActiveQuest = "";
221 230 if (questKey == "/Lotus/Types/Keys/OrokinMoonQuest/OrokinMoonQuestKeyChain") {
231 const att = isRerun
232 ? []
233 : [
234 "/Lotus/Weapons/Tenno/Melee/Swords/StalkerTwo/StalkerTwoSmallSword",
235 "/Lotus/Upgrades/Skins/Sigils/ScarSigil"
236 ];
222 237 await createMessage(inventory.accountOwnerId, [
223 238 {
224 239 sndr: "/Lotus/Language/Bosses/Ordis",
225 240 msg: "/Lotus/Language/G1Quests/SecondDreamFinishInboxMessage",
226 att: [
227 "/Lotus/Weapons/Tenno/Melee/Swords/StalkerTwo/StalkerTwoSmallSword",
228 "/Lotus/Upgrades/Skins/Sigils/ScarSigil"
229 ],
241 att,
230 242 sub: "/Lotus/Language/G1Quests/SecondDreamFinishInboxTitle",
231 243 icon: "/Lotus/Interface/Icons/Npcs/Ordis.png",
232 244 highPriority: true
233 245 }
234 246 ]);
235 } else if (questKey == "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain") {
247 } else if (questKey == "/Lotus/Types/Keys/NewWarQuest/NewWarQuestKeyChain" && !isRerun) {
236 248 setupKahlSyndicate(inventory);
237 249 }
238 250
251 if (isRerun) return;
252
239 253 // Whispers in the Walls is unlocked once The New War + Heart of Deimos are completed.
240 254 if (
241 255 doesQuestCompletionFinishSet(inventory, questKey, [
@@ -279,21 +293,24 @@ const handleQuestCompletion = async (
279 293 if (questCompletionItems) {
280 294 await addItems(inventory, questCompletionItems, inventoryChanges);
281 295 }
282
283 if (inventory.ActiveQuest == questKey) inventory.ActiveQuest = "";
284 296 };
285 297
286 298 export const giveKeyChainItem = async (
287 299 inventory: TInventoryDatabaseDocument,
288 keyChainInfo: IKeyChainRequest
300 keyChainInfo: IKeyChainRequest,
301 isRerun: boolean = false
289 302 ): Promise<IInventoryChanges> => {
290 const inventoryChanges = await addKeyChainItems(inventory, keyChainInfo);
303 let inventoryChanges: IInventoryChanges = {};
304
305 if (!isRerun) {
306 inventoryChanges = await addKeyChainItems(inventory, keyChainInfo);
291 307
292 if (isEmptyObject(inventoryChanges)) {
293 logger.warn("inventory changes was empty after getting keychain items: should not happen");
308 if (isEmptyObject(inventoryChanges)) {
309 logger.warn("inventory changes was empty after getting keychain items: should not happen");
310 }
311 // items were added: update quest stage's i (item was given)
312 updateQuestStage(inventory, keyChainInfo, { i: true });
294 313 }
295 // items were added: update quest stage's i (item was given)
296 updateQuestStage(inventory, keyChainInfo, { i: true });
297 314
298 315 return inventoryChanges;
299 316
@@ -309,12 +326,17 @@ export const giveKeyChainItem = async (
309 326
310 327 export const giveKeyChainMessage = async (
311 328 inventory: TInventoryDatabaseDocument,
312 accountId: string | Types.ObjectId,
313 keyChainInfo: IKeyChainRequest
329 keyChainInfo: IKeyChainRequest,
330 isRerun: boolean = false
314 331 ): Promise<void> => {
315 332 const keyChainMessage = getKeyChainMessage(keyChainInfo);
316 333
317 await createMessage(accountId, [keyChainMessage]);
334 if (!isRerun) {
335 keyChainMessage.att = [];
336 keyChainMessage.countedAtt = [];
337 }
338
339 await createMessage(inventory.accountOwnerId, [keyChainMessage]);
318 340
319 341 updateQuestStage(inventory, keyChainInfo, { m: true });
320 342 };
@@ -328,8 +350,10 @@ export const giveKeyChainMissionReward = async (
328 350
329 351 if (chainStages) {
330 352 const missionName = chainStages[keyChainInfo.ChainStage].key;
331 if (missionName) {
353 const questKey = inventory.QuestKeys.find(q => q.ItemType === keyChainInfo.KeyChain);
354 if (missionName && questKey) {
332 355 const fixedLevelRewards = getLevelKeyRewards(missionName);
356 const run = questKey.Progress?.[0]?.c ?? 0;
333 357 if (fixedLevelRewards.levelKeyRewards) {
334 358 const missionRewards: { StoreItem: string; ItemCount: number }[] = [];
335 359 inventory.RegularCredits += addFixedLevelRewards(fixedLevelRewards.levelKeyRewards, missionRewards);
@@ -338,7 +362,7 @@ export const giveKeyChainMissionReward = async (
338 362 await addItem(inventory, fromStoreItem(reward.StoreItem), reward.ItemCount);
339 363 }
340 364
341 updateQuestStage(inventory, keyChainInfo, { c: 0 });
365 updateQuestStage(inventory, keyChainInfo, { c: run });
342 366 } else if (fixedLevelRewards.levelKeyRewards2) {
343 367 for (const reward of fixedLevelRewards.levelKeyRewards2) {
344 368 if (reward.rewardType == "RT_CREDITS") {
@@ -352,7 +376,7 @@ export const giveKeyChainMissionReward = async (
352 376 }
353 377 }
354 378
355 updateQuestStage(inventory, keyChainInfo, { c: 0 });
379 updateQuestStage(inventory, keyChainInfo, { c: run });
356 380 }
357 381 }
358 382 }
@@ -364,14 +388,17 @@ export const giveKeyChainStageTriggered = async (
364 388 ): Promise<void> => {
365 389 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
366 390 const chainStages = ExportKeys[keyChainInfo.KeyChain]?.chainStages;
391 const questKey = inventory.QuestKeys.find(qk => qk.ItemType === keyChainInfo.KeyChain);
392
393 if (chainStages && questKey) {
394 const run = questKey.Progress?.[0]?.c ?? 0;
367 395
368 if (chainStages) {
369 396 if (chainStages[keyChainInfo.ChainStage].itemsToGiveWhenTriggered.length > 0) {
370 await giveKeyChainItem(inventory, keyChainInfo);
397 await giveKeyChainItem(inventory, keyChainInfo, run > 0);
371 398 }
372 399
373 400 if (chainStages[keyChainInfo.ChainStage].messageToSendWhenTriggered) {
374 await giveKeyChainMessage(inventory, inventory.accountOwnerId, keyChainInfo);
401 await giveKeyChainMessage(inventory, keyChainInfo, run > 0);
375 402 }
376 403 }
377 404 };
Modified src/types/inventoryTypes/inventoryTypes.ts +4 -4
@@ -977,10 +977,10 @@ export interface IQuestKeyClient extends Omit<IQuestKeyDatabase, "CompletionDate
977 977 }
978 978
979 979 export interface IQuestStage {
980 c?: number;
981 i?: boolean;
982 m?: boolean;
983 b?: any[];
980 c: number;
981 i: boolean;
982 m: boolean;
983 b: any[];
984 984 }
985 985
986 986 export interface IRawUpgrade {
Modified static/webui/script.js +6 -1
@@ -989,7 +989,8 @@ function updateInventory() {
989 989 data.QuestKeys.forEach(item => {
990 990 const tr = document.createElement("tr");
991 991 tr.setAttribute("data-item-type", item.ItemType);
992 const stage = item.Progress?.length ?? 0;
992 const run = item.Progress[0]?.c ?? 0;
993 const stage = run == 0 ? item.Progress.length : item.Progress.map(p => p.c ?? 0).lastIndexOf(run);
993 994
994 995 const datalist = document.getElementById("datalist-QuestKeys");
995 996 const optionToRemove = datalist.querySelector(`option[data-key="${item.ItemType}"]`);
@@ -1007,6 +1008,10 @@ function updateInventory() {
1007 1008 td.textContent += " | " + loc("code_completed");
1008 1009 }
1009 1010
1011 if (run > 0) {
1012 td.textContent += " | " + loc("code_replays") + ": " + (run + 1);
1013 }
1014
1010 1015 if (data.ActiveQuest == item.ItemType) td.textContent += " | " + loc("code_active");
1011 1016 tr.appendChild(td);
1012 1017 }
Modified static/webui/translations/de.js +1 -0
@@ -68,6 +68,7 @@ dict = {
68 68 code_unmature: `Genetisches Altern zurücksetzen`,
69 69 code_fund: `[UNTRANSLATED] Fund`,
70 70 code_funded: `[UNTRANSLATED] Funded`,
71 code_replays: `[UNTRANSLATED] Replays`,
71 72 code_succChange: `Erfolgreich geändert.`,
72 73 code_requiredInvigorationUpgrade: `Du musst sowohl ein offensives & defensives Upgrade auswählen.`,
73 74 login_description: `Melde dich mit deinem OpenWF-Account an (denselben Angaben wie im Spiel, wenn du dich mit diesem Server verbindest).`,
Modified static/webui/translations/en.js +1 -0
@@ -67,6 +67,7 @@ dict = {
67 67 code_unmature: `Regress genetic aging`,
68 68 code_fund: `Fund`,
69 69 code_funded: `Funded`,
70 code_replays: `Replays`,
70 71 code_succChange: `Successfully changed.`,
71 72 code_requiredInvigorationUpgrade: `You must select both an offensive & defensive upgrade.`,
72 73 login_description: `Login using your OpenWF account credentials (same as in-game when connecting to this server).`,
Modified static/webui/translations/es.js +1 -0
@@ -68,6 +68,7 @@ dict = {
68 68 code_unmature: `Regresar el envejecimiento genético`,
69 69 code_fund: `[UNTRANSLATED] Fund`,
70 70 code_funded: `[UNTRANSLATED] Funded`,
71 code_replays: `[UNTRANSLATED] Replays`,
71 72 code_succChange: `Cambiado correctamente`,
72 73 code_requiredInvigorationUpgrade: `Debes seleccionar una mejora ofensiva y una defensiva.`,
73 74 login_description: `Inicia sesión con las credenciales de tu cuenta OpenWF (las mismas que usas en el juego al conectarte a este servidor).`,
Modified static/webui/translations/fr.js +1 -0
@@ -68,6 +68,7 @@ dict = {
68 68 code_unmature: `Régrésser l'âge génétique`,
69 69 code_fund: `Financer`,
70 70 code_funded: `Complété`,
71 code_replays: `[UNTRANSLATED] Replays`,
71 72 code_succChange: `Changement effectué.`,
72 73 code_requiredInvigorationUpgrade: `Augmentation offensive et défensive requises.`,
73 74 login_description: `Connexion avec les informations de connexion OpenWF.`,
Modified static/webui/translations/ru.js +1 -0
@@ -68,6 +68,7 @@ dict = {
68 68 code_unmature: `Регрессия генетического старения`,
69 69 code_fund: `Профинансировать`,
70 70 code_funded: `Профинансировано`,
71 code_replays: `Повторов`,
71 72 code_succChange: `Успешно изменено.`,
72 73 code_requiredInvigorationUpgrade: `Вы должны выбрать как атакующее, так и вспомогательное улучшение.`,
73 74 login_description: `Войдите, используя учетные данные OpenWF (те же, что и в игре при подключении к этому серверу).`,
Modified static/webui/translations/uk.js +1 -0
@@ -68,6 +68,7 @@ dict = {
68 68 code_unmature: `Обернути старіння`,
69 69 code_fund: `[UNTRANSLATED] Fund`,
70 70 code_funded: `[UNTRANSLATED] Funded`,
71 code_replays: `[UNTRANSLATED] Replays`,
71 72 code_succChange: `Успішно змінено.`,
72 73 code_requiredInvigorationUpgrade: `Ви повинні вибрати як атакуюче, так і допоміжне вдосконалення.`,
73 74 login_description: `Увійдіть, використовуючи облікові дані OpenWF (ті ж, що й у грі при підключенні до цього серверу).`,
Modified static/webui/translations/zh.js +1 -0
@@ -68,6 +68,7 @@ dict = {
68 68 code_unmature: `逆转衰老基因`,
69 69 code_fund: `[UNTRANSLATED] Fund`,
70 70 code_funded: `[UNTRANSLATED] Funded`,
71 code_replays: `[UNTRANSLATED] Replays`,
71 72 code_succChange: `更改成功`,
72 73 code_requiredInvigorationUpgrade: `您必须同时选择一个进攻型和一个功能型活化属性.`,
73 74 login_description: `使用您的 OpenWF 账户凭证登录(与游戏内连接本服务器时使用的昵称相同)`,