From b62e326920b335a892da8548e1f6a58180ec4f2d Mon Sep 17 00:00:00 2001 From: AMelonInsideLemon <166175391+AMelonInsideLemon@users.noreply.github.com> Date: Tue, 29 Jul 2025 00:31:29 -0700 Subject: [PATCH] feat(webui): ability overrides (#2558) Closes #851 Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/2558 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> --- .../custom/abilityOverrideController.ts | 33 +++++ .../custom/getItemListsController.ts | 18 ++- src/routes/custom.ts | 2 + static/webui/index.html | 8 + static/webui/script.js | 138 ++++++++++++++++++ static/webui/translations/de.js | 4 + static/webui/translations/en.js | 4 + static/webui/translations/es.js | 4 + static/webui/translations/fr.js | 4 + static/webui/translations/ru.js | 4 + static/webui/translations/zh.js | 4 + 11 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 src/controllers/custom/abilityOverrideController.ts diff --git a/src/controllers/custom/abilityOverrideController.ts b/src/controllers/custom/abilityOverrideController.ts new file mode 100644 index 00000000..e67afb1e --- /dev/null +++ b/src/controllers/custom/abilityOverrideController.ts @@ -0,0 +1,33 @@ +import { getInventory } from "@/src/services/inventoryService"; +import { getAccountIdForRequest } from "@/src/services/loginService"; +import { TEquipmentKey } from "@/src/types/inventoryTypes/inventoryTypes"; +import { RequestHandler } from "express"; + +export const abilityOverrideController: RequestHandler = async (req, res) => { + const accountId = await getAccountIdForRequest(req); + const request = req.body as IAbilityOverrideRequest; + if (request.category === "Suits") { + const inventory = await getInventory(accountId, request.category); + const item = inventory[request.category].id(request.oid); + if (item) { + if (request.action == "set") { + item.Configs[request.configIndex].AbilityOverride = request.AbilityOverride; + } else { + item.Configs[request.configIndex].AbilityOverride = undefined; + } + await inventory.save(); + } + } + res.end(); +}; + +interface IAbilityOverrideRequest { + category: TEquipmentKey; + oid: string; + action: "set" | "remove"; + configIndex: number; + AbilityOverride: { + Ability: string; + Index: number; + }; +} diff --git a/src/controllers/custom/getItemListsController.ts b/src/controllers/custom/getItemListsController.ts index d065d2e2..6fe5b0db 100644 --- a/src/controllers/custom/getItemListsController.ts +++ b/src/controllers/custom/getItemListsController.ts @@ -1,6 +1,7 @@ import { RequestHandler } from "express"; import { getDict, getItemName, getString } from "@/src/services/itemDataService"; import { + ExportAbilities, ExportArcanes, ExportAvionics, ExportBoosters, @@ -57,6 +58,7 @@ interface ItemLists { mods: ListedItem[]; Boosters: ListedItem[]; VarziaOffers: ListedItem[]; + Abilities: ListedItem[]; //circuitGameModes: ListedItem[]; } @@ -94,7 +96,8 @@ const getItemListsController: RequestHandler = (req, response) => { EvolutionProgress: [], mods: [], Boosters: [], - VarziaOffers: [] + VarziaOffers: [], + Abilities: [] /*circuitGameModes: [ { uniqueName: "Survival", @@ -132,6 +135,12 @@ const getItemListsController: RequestHandler = (req, response) => { name: getString(item.name, lang), exalted: item.exalted }); + item.abilities.forEach(ability => { + res.Abilities.push({ + uniqueName: ability.uniqueName, + name: getString(ability.name || uniqueName, lang) + }); + }); } for (const [uniqueName, item] of Object.entries(ExportSentinels)) { if (item.productCategory == "Sentinels" || item.productCategory == "KubrowPets") { @@ -348,6 +357,13 @@ const getItemListsController: RequestHandler = (req, response) => { }); } + for (const [uniqueName, ability] of Object.entries(ExportAbilities)) { + res.Abilities.push({ + uniqueName, + name: getString(ability.name || uniqueName, lang) + }); + } + response.json(res); }; diff --git a/src/routes/custom.ts b/src/routes/custom.ts index 9eb3d9e4..acd5c834 100644 --- a/src/routes/custom.ts +++ b/src/routes/custom.ts @@ -15,6 +15,7 @@ import { webuiFileChangeDetectedController } from "@/src/controllers/custom/webu import { completeAllMissionsController } from "@/src/controllers/custom/completeAllMissionsController"; import { addMissingHelminthBlueprintsController } from "@/src/controllers/custom/addMissingHelminthBlueprintsController"; +import { abilityOverrideController } from "@/src/controllers/custom/abilityOverrideController"; import { createAccountController } from "@/src/controllers/custom/createAccountController"; import { createMessageController } from "@/src/controllers/custom/createMessageController"; import { addCurrencyController } from "@/src/controllers/custom/addCurrencyController"; @@ -47,6 +48,7 @@ customRouter.get("/webuiFileChangeDetected", webuiFileChangeDetectedController); customRouter.get("/completeAllMissions", completeAllMissionsController); customRouter.get("/addMissingHelminthBlueprints", addMissingHelminthBlueprintsController); +customRouter.post("/abilityOverride", abilityOverrideController); customRouter.post("/createAccount", createAccountController); customRouter.post("/createMessage", createMessageController); customRouter.post("/addCurrency", addCurrencyController); diff --git a/static/webui/index.html b/static/webui/index.html index ba2c4b93..0e0a6941 100644 --- a/static/webui/index.html +++ b/static/webui/index.html @@ -460,6 +460,13 @@

+
+
+
+ +
+
+
@@ -1073,6 +1080,7 @@ + diff --git a/static/webui/script.js b/static/webui/script.js index dc5af056..ae803994 100644 --- a/static/webui/script.js +++ b/static/webui/script.js @@ -1241,6 +1241,117 @@ function updateInventory() { document.getElementById("dv-invigoration-offensive").value = OffensiveUpgrade; document.getElementById("dv-invigoration-defensive").value = DefensiveUpgrade; document.getElementById("dv-invigoration-expiry").value = UpgradesExpiry; + + { + document.getElementById("loadout-card").classList.remove("d-none"); + const maxModConfigNum = Math.min(2 + (item.ModSlotPurchases ?? 0), 5); + + const configs = item.Configs ?? []; + + const loadoutTabs = document.getElementById("loadoutTabs"); + const loadoutTabsContent = document.getElementById("loadoutTabsContent"); + loadoutTabs.innerHTML = ""; + loadoutTabsContent.innerHTML = ""; + for (let i = 0; i <= maxModConfigNum; i++) { + const config = configs[i] ?? {}; + + { + const li = document.createElement("li"); + li.classList.add("nav-item"); + + const button = document.createElement("button"); + button.classList.add("nav-link"); + if (i === 0) button.classList.add("active"); + button.id = `config${i}-tab`; + button.setAttribute("data-bs-toggle", "tab"); + button.setAttribute("data-bs-target", `#config${i}`); + button.innerHTML = config.Name?.trim() || String.fromCharCode(65 + i); + + li.appendChild(button); + loadoutTabs.appendChild(li); + } + + { + const tabDiv = document.createElement("div"); + tabDiv.classList = "tab-pane"; + if (i === 0) tabDiv.classList.add("show", "active"); + + tabDiv.id = `config${i}`; + + { + const abilityOverrideForm = document.createElement("form"); + abilityOverrideForm.classList = "form-group mt-2"; + abilityOverrideForm.setAttribute( + "onsubmit", + `handleAbilityOverride(event, ${i});return false;` + ); + + const abilityOverrideFormLabel = document.createElement("label"); + abilityOverrideFormLabel.setAttribute("data-loc", "abilityOverride_label"); + abilityOverrideFormLabel.innerHTML = loc("abilityOverride_label"); + abilityOverrideFormLabel.classList = "form-label"; + abilityOverrideFormLabel.setAttribute("for", "abilityOverride-ability"); + abilityOverrideForm.appendChild(abilityOverrideFormLabel); + + const abilityOverrideInputGroup = document.createElement("div"); + abilityOverrideInputGroup.classList = "input-group"; + abilityOverrideForm.appendChild(abilityOverrideInputGroup); + + const abilityOverrideInput = document.createElement("input"); + abilityOverrideInput.id = "abilityOverride-ability"; + abilityOverrideInput.classList = "form-control"; + abilityOverrideInput.setAttribute("list", "datalist-Abilities"); + if (config.AbilityOverride) { + const datalist = document.getElementById("datalist-Abilities"); + const options = Array.from(datalist.options); + abilityOverrideInput.value = options.find( + option => + config.AbilityOverride.Ability == option.getAttribute("data-key") + ).value; + } + abilityOverrideInputGroup.appendChild(abilityOverrideInput); + + const abilityOverrideOnSlot = document.createElement("span"); + abilityOverrideOnSlot.classList = "input-group-text"; + abilityOverrideOnSlot.setAttribute("data-loc", "abilityOverride_onSlot"); + abilityOverrideOnSlot.innerHTML = loc("abilityOverride_onSlot"); + abilityOverrideInputGroup.appendChild(abilityOverrideOnSlot); + + const abilityOverrideSecondInput = document.createElement("input"); + abilityOverrideSecondInput.id = "abilityOverride-ability-index"; + abilityOverrideSecondInput.classList = "form-control"; + abilityOverrideSecondInput.setAttribute("type", "number"); + abilityOverrideSecondInput.setAttribute("min", "0"); + abilityOverrideSecondInput.setAttribute("max", "3"); + if (config.AbilityOverride) + abilityOverrideSecondInput.value = config.AbilityOverride.Index; + abilityOverrideInputGroup.appendChild(abilityOverrideSecondInput); + + const abilityOverrideSetButton = document.createElement("button"); + abilityOverrideSetButton.classList = "btn btn-primary"; + abilityOverrideSetButton.setAttribute("type", "submit"); + abilityOverrideSetButton.setAttribute("value", "set"); + abilityOverrideSetButton.setAttribute("data-loc", "general_setButton"); + abilityOverrideSetButton.innerHTML = loc("general_setButton"); + abilityOverrideInputGroup.appendChild(abilityOverrideSetButton); + + const abilityOverrideRemoveButton = document.createElement("button"); + abilityOverrideRemoveButton.classList = "btn btn-danger"; + abilityOverrideRemoveButton.setAttribute("type", "submit"); + abilityOverrideRemoveButton.setAttribute("value", "remove"); + abilityOverrideRemoveButton.setAttribute("data-loc", "code_remove"); + abilityOverrideRemoveButton.innerHTML = loc("code_remove"); + abilityOverrideInputGroup.appendChild(abilityOverrideRemoveButton); + + abilityOverrideForm.appendChild(abilityOverrideInputGroup); + + tabDiv.appendChild(abilityOverrideForm); + } + + loadoutTabsContent.appendChild(tabDiv); + } + } + } } else if (["LongGuns", "Pistols", "Melee", "SpaceGuns", "SpaceMelee"].includes(category)) { document.getElementById("valenceBonus-card").classList.remove("d-none"); document.getElementById("valenceBonus-innateDamage").value = ""; @@ -2248,6 +2359,7 @@ single.getRoute("#detailedView-route").on("beforeload", function () { document.getElementById("detailedView-loading").classList.remove("d-none"); document.getElementById("detailedView-title").textContent = ""; document.querySelector("#detailedView-route .text-body-secondary").textContent = ""; + document.getElementById("loadout-card").classList.add("d-none"); document.getElementById("archonShards-card").classList.add("d-none"); document.getElementById("edit-suit-invigorations-card").classList.add("d-none"); document.getElementById("modularParts-card").classList.add("d-none"); @@ -2963,3 +3075,29 @@ async function editSuitInvigorationUpgrade(oid, data) { updateInventory(); }); } + +function handleAbilityOverride(event, configIndex) { + event.preventDefault(); + const urlParams = new URLSearchParams(window.location.search); + const action = event.submitter.value; + const Ability = getKey(document.getElementById("abilityOverride-ability")); + const Index = document.getElementById("abilityOverride-ability-index").value; + revalidateAuthz().then(() => { + $.post({ + url: "/custom/abilityOverride?" + window.authz, + contentType: "application/json", + data: JSON.stringify({ + category: urlParams.get("productCategory"), + oid: urlParams.get("itemId"), + configIndex, + action, + AbilityOverride: { + Ability, + Index + } + }) + }).done(function () { + updateInventory(); + }); + }); +} diff --git a/static/webui/translations/de.js b/static/webui/translations/de.js index aa35bcd9..c23b8f61 100644 --- a/static/webui/translations/de.js +++ b/static/webui/translations/de.js @@ -128,6 +128,7 @@ dict = { detailedView_valenceBonusDescription: `Du kannst den Valenz-Bonus deiner Waffe festlegen oder entfernen.`, detailedView_modularPartsLabel: `Modulare Teile ändern`, detailedView_suitInvigorationLabel: `Warframe-Kräftigung`, + detailedView_loadoutLabel: `Loadouts`, invigorations_offensive_AbilityStrength: `+200% Fähigkeitsstärke`, invigorations_offensive_AbilityRange: `+100% Fähigkeitsreichweite`, @@ -155,6 +156,9 @@ dict = { invigorations_defensiveLabel: `Defensives Upgrade`, invigorations_expiryLabel: `Upgrades Ablaufdatum (optional)`, + abilityOverride_label: `[UNTRANSLATED] Ability Override`, + abilityOverride_onSlot: `[UNTRANSLATED] on slot`, + mods_addRiven: `Riven hinzufügen`, mods_fingerprint: `Fingerabdruck`, mods_fingerprintHelp: `Benötigst du Hilfe mit dem Fingerabdruck?`, diff --git a/static/webui/translations/en.js b/static/webui/translations/en.js index 12c5c9fd..b825f923 100644 --- a/static/webui/translations/en.js +++ b/static/webui/translations/en.js @@ -127,6 +127,7 @@ dict = { detailedView_valenceBonusDescription: `You can set or remove the Valence Bonus from your weapon.`, detailedView_modularPartsLabel: `Change Modular Parts`, detailedView_suitInvigorationLabel: `Warframe Invigoration`, + detailedView_loadoutLabel: `Loadouts`, invigorations_offensive_AbilityStrength: `+200% Ability Strength`, invigorations_offensive_AbilityRange: `+100% Ability Range`, @@ -154,6 +155,9 @@ dict = { invigorations_defensiveLabel: `Defensive Upgrade`, invigorations_expiryLabel: `Upgrades Expiry (optional)`, + abilityOverride_label: `Ability Override`, + abilityOverride_onSlot: `on slot`, + mods_addRiven: `Add Riven`, mods_fingerprint: `Fingerprint`, mods_fingerprintHelp: `Need help with the fingerprint?`, diff --git a/static/webui/translations/es.js b/static/webui/translations/es.js index 881592f2..84390567 100644 --- a/static/webui/translations/es.js +++ b/static/webui/translations/es.js @@ -128,6 +128,7 @@ dict = { detailedView_valenceBonusDescription: `Puedes establecer o quitar el bono de valencia de tu arma.`, detailedView_modularPartsLabel: `Cambiar partes modulares`, detailedView_suitInvigorationLabel: `Vigorización de Warframe`, + detailedView_loadoutLabel: `Equipamientos`, invigorations_offensive_AbilityStrength: `+200% Fuerza de Habilidad`, invigorations_offensive_AbilityRange: `+100% Alcance de Habilidad`, @@ -155,6 +156,9 @@ dict = { invigorations_defensiveLabel: `Mejora Defensiva`, invigorations_expiryLabel: `Caducidad de Mejoras (opcional)`, + abilityOverride_label: `[UNTRANSLATED] Ability Override`, + abilityOverride_onSlot: `[UNTRANSLATED] on slot`, + mods_addRiven: `Agregar Agrietado`, mods_fingerprint: `Huella digital`, mods_fingerprintHelp: `¿Necesitas ayuda con la huella digital?`, diff --git a/static/webui/translations/fr.js b/static/webui/translations/fr.js index 26c8fc68..626591f5 100644 --- a/static/webui/translations/fr.js +++ b/static/webui/translations/fr.js @@ -128,6 +128,7 @@ dict = { detailedView_valenceBonusDescription: `Définir le Bonus Valence de l'arme.`, detailedView_modularPartsLabel: `Changer l'équipement modulaire`, detailedView_suitInvigorationLabel: `Invigoration de Warframe`, + detailedView_loadoutLabel: `Équipements`, invigorations_offensive_AbilityStrength: `+200% de puissance de pouvoir`, invigorations_offensive_AbilityRange: `+100% de portée de pouvoir`, @@ -155,6 +156,9 @@ dict = { invigorations_defensiveLabel: `Amélioration défensive`, invigorations_expiryLabel: `Expiration de l'invigoration (optionnel)`, + abilityOverride_label: `[UNTRANSLATED] Ability Override`, + abilityOverride_onSlot: `[UNTRANSLATED] on slot`, + mods_addRiven: `Ajouter un riven`, mods_fingerprint: `Empreinte`, mods_fingerprintHelp: `Besoin d'aide pour l'empreinte ?`, diff --git a/static/webui/translations/ru.js b/static/webui/translations/ru.js index 9b3d42a2..5af30117 100644 --- a/static/webui/translations/ru.js +++ b/static/webui/translations/ru.js @@ -128,6 +128,7 @@ dict = { detailedView_valenceBonusDescription: `Вы можете установить или убрать бонус валентности с вашего оружия.`, detailedView_modularPartsLabel: `Изменить Модульные Части`, detailedView_suitInvigorationLabel: `[UNTRANSLATED] Warframe Invigoration`, + detailedView_loadoutLabel: `Конфигурации`, invigorations_offensive_AbilityStrength: `[UNTRANSLATED] +200% Ability Strength`, invigorations_offensive_AbilityRange: `[UNTRANSLATED] +100% Ability Range`, @@ -155,6 +156,9 @@ dict = { invigorations_defensiveLabel: `[UNTRANSLATED] Defensive Upgrade`, invigorations_expiryLabel: `[UNTRANSLATED] Upgrades Expiry (optional)`, + abilityOverride_label: `Переопределение способности`, + abilityOverride_onSlot: `в ячейке`, + mods_addRiven: `Добавить Мод Разлома`, mods_fingerprint: `Отпечаток`, mods_fingerprintHelp: `Нужна помощь с отпечатком?`, diff --git a/static/webui/translations/zh.js b/static/webui/translations/zh.js index bb000f25..e8e486b4 100644 --- a/static/webui/translations/zh.js +++ b/static/webui/translations/zh.js @@ -128,6 +128,7 @@ dict = { detailedView_valenceBonusDescription: `您可以设置或移除武器上的效价加成.`, detailedView_modularPartsLabel: `更换部件`, detailedView_suitInvigorationLabel: `编辑战甲活化属性`, + detailedView_loadoutLabel: `配置`, invigorations_offensive_AbilityStrength: `+200%技能强度`, invigorations_offensive_AbilityRange: `+100%技能范围`, @@ -155,6 +156,9 @@ dict = { invigorations_defensiveLabel: `功能型属性`, invigorations_expiryLabel: `活化时效(可选)`, + abilityOverride_label: `[UNTRANSLATED] Ability Override`, + abilityOverride_onSlot: `[UNTRANSLATED] on slot`, + mods_addRiven: `添加裂罅MOD`, mods_fingerprint: `印记`, mods_fingerprintHelp: `需要印记相关的帮助?`,